Fullstack React Native
The Complete Guide to React Native
Wri!en by Devin Abbo!, Houssein Djirdeh, Anthony Accomazzo, and Sophia
Shoemaker
© 2017 Fullstack.io
All rights reserved. No portion of the book manuscript may be reproduced, stored in a retrieval
system, or transmi!ed in any form or by any means beyond the number of purchased copies,
except for a single backup or archival copy. "e code may be used freely in your projects,
commercial or otherwise.
"e authors and publisher have taken care in preparation of this book, but make no expressed
or implied warranty of any kind and assume no responsibility for errors or omissions. No
liability is assumed for incidental or consequential damagers in connection with or arising out
of the use of the information or programs container herein.
Published in San Francisco, California by Fullstack.io.
FULLSTACK
.io
Contents
Book Revision . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Bug Reports . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Be notified of updates via Twitter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
We’d love to hear from you! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
About This Book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Running Code Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Code Blocks and Context . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Getting Help . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Emailing Us . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Getting Started with React Native . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Weather App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Starting the project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Expo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Custom components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
React Fundamentals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Breaking the app into components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
7 step process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Step 2: Build a static version of the app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Step 3: Determine what should be stateful . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Step 4: Determine in which component each piece of state should live . . . . . . . . . . . . 93
Step 5: Hardcode initial states . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Step 6: Add inverse data flow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Updating timers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Deleting timers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Adding timing functionality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
Add start and stop functionality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
Methodology review . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
Core Components, Part 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
CONTENTS
What are components? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
Building an Instagram clone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
StyleSheet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
TouchableOpacity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
ActivityIndicator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
FlatList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Core Components, Part 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
TextInput . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
ScrollView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Modal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
Core APIs, Part 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
Building a messaging app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
Initializing the project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
The app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Network connectivity indicator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
The message list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Toolbar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
Geolocation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
Input Method Editor (IME) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Core APIs, Part 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
The keyboard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
We’re Done! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Navigation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
Navigation in React Native . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
Contact List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
Starting the project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
Container and Presentational components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324
Contacts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324
Profile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
React Navigation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
Stack navigation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
Tab navigation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
Drawer navigation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 364
Sharing state between screens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Deep Linking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
CONTENTS
Animation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384
Animation challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384
Building a puzzle game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386
App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390
Building the Start screen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 392
Building the Game screen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
Gestures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430
Building the board . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430
Gesture Responder System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
PanResponder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 446
Draggable component . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447
Finishing the game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 461
We’re Done! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466
Native Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
What are native modules? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
Building a native module . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469
Development environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471
Initializing the project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 472
iOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477
Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 487
JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
Building and publishing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 504
How to read this chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 504
Building . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 504
Building with Expo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
iOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 508
Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 525
Handling Updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 532
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533
Appendix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 534
JavaScript Versions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 534
ES2015 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 534
ReactElement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 540
Handling Events in React Native . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 541
Publishing with Expo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 545
Changelog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 546
CONTENTS 1
Book Revision
Revision 5 - Native modules chapter added to book
Bug Reports
If you’d like to report any bugs, typos, or suggestions just email us at: rn@fullstack.io
1
.
Be notified of updates via Twitter
If you’d like to be notified of updates to the book on Twitter, follow @fullstackreact
2
We’d love to hear from you!
Did you like the book? Did you find it helpful? We’d love to add your face to our list of testimonials
on the website! Email us at: rn@fullstack.io
3
.
1
mailto:rn@fullstack.io?Subject=Fullstack%20React%20Native%20book%20feedback
2
https://twitter.com/fullstackreact
3
mailto:rn@fullstack.io?Subject=React%20Native%20testimonial
Introduction
One of the major problems that teams face when writing native mobile applications is becoming
familiar with all the different technologies. iOS and Android - the two dominant mobile platforms
- support different languages. For iOS, Apple supports the languages Swift
4
and Objective-C
5
. For
Android, Google supports the languages Java
6
and Kotlin
7
.
And the differences don’t end there. These platforms have different toolchains. And they have
different interfaces for the device’s core functionality. Developers have to learn each platform’s
procedure for things like accessing the camera or checking network connectivity.
One trend is to write mobile apps that are powered by WebViews. These types of apps have minimal
native code. Instead, the interface is a web browser running an app written in HTML, CSS, and JS.
This web app can use the native wrapper to access features on the device, like the camera roll.
Tools like Cordova
8
enable developers to write these hybrid apps. The advantage is that developers
can write apps that run on multiple platforms. Instead of learning iOS and Android specifics, they
can use HTML, CSS, and JS to write a “universal” app.
The disadvantage, though, is that it’s hard to make these apps look and feel like real native
applications. And users can tell.
While universal WebView-powered apps were built with the idea of build once, run anywhere, React
Native was built with the goal of learn once, write anywhere.
React is a JavaScript framework for building rich, interactive web applications. With React
Native, we can build native mobile applications for multiple platforms using JavaScript and React.
Importantly, the interfaces we build are translated into native views. React Native apps are not
composed of WebViews.
We’ll be able to share a lot of the code we write between iOS and Android. And React Native makes
it easy to write code specific to each platform when the need arises. We get to use one language
(JavaScript), one framework (React), one styling engine, and one toolchain to write apps for both
platforms. Learn once, write anywhere.
At its core, React Native is composed of React components. We’ll dig deep into components
throughout this book, but here’s an example of what a React component looks like:
4
https://developer.apple.com/swift/
5
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/
Introduction.html
6
https://docs.oracle.com/javase/8/docs/technotes/guides/language/index.html
7
https://developer.android.com/kotlin/index.html
8
https://cordova.apache.org/
Introduction 2
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default class StyledText extends React.Component {
render() {
return (
<Text style={styles.text}>{content}</Text>
);
}
}
const styles = StyleSheet.create({
text: {
color: 'red',
fontWeight: 'bold',
},
});
React Native works. It is currently being used in production at Facebook, Instagram, Airbnb, and
thousands of other companies.
About This Book
This book aims to be an extensive React Native resource. By the time you’re done reading this book,
you (and your team) will have everything you need to build reliable React Native applications.
React Native is rich and feature-filled, but that also means it can be tricky to understand all of its
parts. In this book, we’ll walk through everything, such as installing its tools, writing components,
navigating between screens, and integrating native modules.
But before we dig in, there are a few guidelines we want to give you in order to get the most out of
this book. Specifically:
how to approach the code examples and
how to get help if something goes wrong
Running Code Examples
This book comes with a library of runnable code examples. The code is available to download from
the same place where you downloaded this book.
We use yarn
9
to run every example in this book. This means you can type the following commands
to run any example:
9
https://yarnpkg.com/en/
Introduction 3
yarn start will start the React Native packager and print a QR code. If you’re on an Android
mobile device, scanning this code with the Expo
10
app will load the application. For iOS devices,
see the instructions for loading apps onto your phone at the beginning of the first chapter.
yarn run ios will start the React Native packager and open your app in the iOS Simulator if
you are using a Mac.
yarn run android will start the React Native packager and open your app on a connected
Android device or emulator.
In the next chapter we’ll explain each of these commands in detail.
Code Blocks and Context
Nearly every code block in this book is pulled from a runnable code example, which you can find
in the sample code. For example, here is a code block pulled from the first chapter:
weather/1/App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<Text>Changes you make will automatically reload.</Text>
<Text>Shake your phone to open the developer menu.</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
10
https://expo.io/
Introduction 4
Notice that the header of this code block states the path to the file which contains this code:
code/weather/1/App.js.
This book is written with the expectation that you’ll also be looking at the example code
alongside the chapter. If you ever feel like you’re missing the context for a code example, open up
the full code file using your favorite text editor.
For example, we often need to import libraries to get our code to run. In the early chapters of the
book we show these import statements, because it’s not clear where the libraries are coming from
otherwise. However, the later chapters of the book are more advanced and they focus on key concepts
instead of repeating boilerplate code that was covered earlier in the book. If at any point you’re not
clear on the context, open up the code example on disk.
Getting Help
While we’ve made every effort to be clear, precise, and accurate you may find that when you’re
writing your code you run into a problem.
Generally, there are three types of problems:
A “bug” in the book (e.g. something is explained incorrectly)
A “bug” in our code
A “bug” in your code
If you find an inaccuracy in our description of something, or you feel a concept isn’t clear, email us!
We want to make sure that the book is both accurate and clear.
Similarly, if you’ve found a bug in our code we definitely want to hear about it.
If you’re having trouble getting your own app working (and it isn’t our example code), this case is a
bit harder for us to handle. If you’re still stuck, we’d still love to hear from you, and here some tips
for getting a clear, timely response.
Emailing Us
If you’re emailing us asking for technical help, here’s what we’d like to know:
What revision of the book are you referring to?
What operating system are you on? (e.g. Mac OS X 10.8, Windows 95)
Which chapter and which example project are you on?
What were you trying to accomplish?
What have you tried already?
What output did you expect?
Introduction 5
What actually happened? (Including relevant log output.)
The absolute best way to get technical support is to send us a short, self-contained example of the
problem. Our preferred way to receive this would be for you to send us an Expo Snack link
11
. Snack
is an online code editor that let’s one quickly develop and demo React Native components on the
browser or an actual device without having to set up a brand new project. We’ll explain Expo in
more detail in the next chapter.
When you’ve written down these things, email us at rn@fullstack.io. We look forward to hearing
from you.
11
https://snack.expo.io/
Getting Started with React Native
Weather App
In this chapter we’re going to build a weather application that allows the user to search for any city
and view its current forecast.
With this simple app we’ll cover some essentials of React Native including:
Using core and custom components
Passing data between components
Handling component state
Handling user input
Applying styles to components
Fetching data from a remote API
By the time we’re finished with this chapter, you’ll know how to get started with Create React Native
App and build a basic application with local state management. You’ll have the foundation you need
to build a wide variety of your own React Native apps.
Here’s a screenshot of what our app will look like when it’s done:
Getting Started with React Native 7
The completed app
In this chapter, we’ll build an entire React Native application from scratch. We’ll talk about how to
set up our development environment and how to initialize a new React Native application. We’ll
also learn how Expo allows us to rapidly prototype and preview our application on our mobile
device. After covering some of the basics of React Native, we’ll explore how we compose apps using
components. Components are a powerful paradigm for organizing views and managing dynamic
data.
We’re about to touch on a wide variety of topics, like styling and data management. This chapter
will exhibit how all these topics fit together at a high-level. In subsequent chapters, we’ll dive deep
into the concepts that we touch on here.
Code examples
This book is example-driven. Each chapter is setup as a hands-on tutorial.
We’ll be building apps from the ground up. Included with this book is a download that contains
completed versions of each app as well as each of the versions we develop along the way (the “sample
code.”) If you’re following along, we recommend you use the sample code for copying and pasting
longer examples or debugging unexpected errors. If you’re not following along, you can refer to the
sample code for more context around a given code example.
The structure of the sample code for all the chapters in this book follows this pattern:
Getting Started with React Native 8
├── components/
├── App.js
├── 1/
├── components/
└── App.js
├── 2/
├── components/
└── App.js
├── 3/
├── components/
└── App.js
// ...
At the top-level of the directory is App.js and components/. This is the code for the completed
version of the application. Inside the numbered folders (1/, 2/, 3/) are the different versions of the
app as we build it up throughout the chapter.
Here’s what a code example in this book looks like:
weather/1/App.js
render() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<Text>Changes you make will automatically reload.</Text>
<Text>Shake your phone to open the developer menu.</Text>
</View>
);
}
Note that the title of the code block contains the path within the sample code where you can find
this example (weather/1/App.js).
JavaScript
This book assumes some JavaScript knowledge.
React Native uses Babel
12
as a JavaScript compiler to allow us to develop in the latest version
of JavaScript, ES2016. To understand what we mean by JavaScript versions, you can refer to the
Appendix.
We highlight some of JavaScript’s newer features in the Appendix. We reference the appendix when
relevant.
12
https://babeljs.io/
Getting Started with React Native 9
Starting the project
Create React Native App
To begin, we’re going to use Create React Native App
13
(CRNA), a tool that makes it extremely easy
to get started with React Native. If you’ve used Create React App
14
before, you’ll notice similarities
here in that no build configuration is required to get up and running. We can install it globally using
yarn
15
.
yarn
yarn
16
is a node package manager that automates the process of managing all the required
dependencies and packages from npm, an online repository of published JavaScript libraries and
projects, in an application. This is done by defining all our dependencies in a single package.json
file.
npm also has a command line tool, npm, that allows us to maintain and control dependencies.
The tool that we use to build our application, CRNA, does not currently work with the latest
version of npm, npm v5. For this reason, we’ll use yarn throughout the book.
You can refer to the documentation
17
for instructions to install yarn for your operating system. The
documentation also explains how to install node
18
as well. In order to use CRNA however, Node.js
v6 or later is required.
Here’s a list of some commonly used yarn commands:
yarn init creates a package.json file and adds it directly to our project.
yarn installs all the dependencies listed in package.json into a local node_modules folder.
yarn add new-package will install a specific package to our project as well as include it as a
dependency in package.json. Dependencies are packages needed when we run our code.
yarn add new-package --dev will install a specific package to our project as well as include it as
a development dependency in package.json. Development dependencies are packages needed
only during the development workflow. They are not needed for running our application in
production.
yarn global add new-package will install the package globally, rather than locally to a specific
project. This is useful when we need to use a command line tool anywhere on our machine.
13
https://github.com/react-community/create-react-native-app
14
https://github.com/facebookincubator/create-react-app
15
https://yarnpkg.com
16
https://yarnpkg.com
17
https://yarnpkg.com/lang/en/docs/install
18
https://nodejs.org/en/
Getting Started with React Native 10
If we already have an older version of npm than v5 installed, we can use it instead of yarn
and run its equivalent commands
19
.
Watchman
Watchman
20
is a file watching service that watches files and triggers actions when they are modified.
If you use macOS as your operating system, the Expo and React Native documentation recommend
installing Watchman for better performance. The instructions to install the service can be found
here
21
.
Expo
Expo
22
is a platform that provides a number of different tools to build fully functional React Native
applications without having to write native code. Beginning a project with CRNA automatically
creates an application that leverages Expo’s development environment.
A benefit of leveraging Expo is that building an application does not require using Xcode for iOS, or
Android Studio for Android. This means that developers can build native iOS applications without
even owning a Mac computer. Using CRNA and Expo is the easiest way to get started with React
Native and is recommended in the React Native documentation
23
.
Including Native Code
Using Expo and CRNA isn’t the only way to start a React Native application. If we need to start
a project with the ability to include native code, we’ll need to use the React Native CLI
a
instead.
With this however, our application will require Xcode and Android Studio for iOS and Android
respectively.
Expo also provides a number of different APIs for device specific properties such as contacts, camera
and video. However, if we need to include a native iOS or Android dependency that is not provided
by Expo, we’ll need to eject from the platform entirely. Ejecting an Expo application means we have
full control of managing our native dependencies, but we would need to use the React Native CLI
from that point on.
We’ll explore how to add native modules onto a React Native project later on this book.
a
https://facebook.github.io/react-native/docs/getting-started.html#installing-dependencies
19
https://yarnpkg.com/lang/en/docs/migrating-from-npm/#toc-cli-commands-comparison
20
https://facebook.github.io/watchman/
21
https://facebook.github.io/watchman/docs/install.html#installing-on-os-x-via-homebrew
22
https://expo.io/
23
https://facebook.github.io/react-native/docs/getting-started.html
Getting Started with React Native 11
Previewing the app
To develop and preview apps with Expo, we need to install its client iOS or Android app
24
to develop
and run React Native apps on our device.
Android
On your Android mobile device, install the Expo Client on Google Play
25
. You can then select Scan
QR code and scan this QR code once you’ve installed the app:
QR Code
If this QR code doesn’t work, we recommend making sure you have the latest version of that Expo
app installed, and that you’re reading the latest edition of this book.
Instead of scanning the QR code, you can also type the project URL,
exp://exp.host/@fullstackio/weather
, inside of Expo to load the application.
iOS
You can install the Expo Client via the App Store
26
. With an iOS device however, there is no capability
to scan a QR code. This means we’ll first need to build the final app in order to preview it. We can
do this by navigating to the weather/ directory in the sample code folder and running the following
commands:
cd weather
yarn
yarn start
This will start the React Native packager. Pressing s will allow you to send a link to your device
by SMS or e-mail (you’ll need to provide your mobile phone number of email address). Once done,
clicking the link will open the application in the Expo Client.
24
https://github.com/expo/expo
25
https://play.google.com/store/apps/details?id=host.exp.exponent
26
https://itunes.apple.com/us/app/expo-client/id982107779?mt=8
Getting Started with React Native 12
For the app to load on your physical device, you’ll need to make sure that your phone is connected
to the same local network as your computer.
Local Development Tool
In addition to a client app, Expo also provides two local development tools that allow us to
preview, share and publish our projects:
XDE
27
, or the Expo Development Environment, is a desktop app that we can use for
macOS, Windows, or Linux.
exp
28
is a command line interface.
Both options provide a number of different commands and services that we can use to
manage our applications. Instead of using the s hotkey provided by CRNA when we start
the packager to send the link to an iOS device, we can also use exp or XDE instead.
We’ll explore using these local development tools in more detail when we cover deploying
and publishing apps later in this book.
Preparing the app
At this point, you should see the final application load successfully on your device. Play around with
the app for a few minutes to get a feel for it. Try searching for different cities as well a location that
doesn’t even exist.
If you plan on building the application as you read through the chapter, you’ll need to create a brand
new project. Once yarn is installed, let’s run the following command to install Create React Native
App (CRNA) globally:
yarn global add create-react-native-app@1.0.0
The @1.0.0 specifies the version of create-react-native-app to install. It’s important to
lock in version 1.0.0 so that the version on your machine matches that here in the book.
We’ll call our application weather and can use the following command to get started (this command
may take a little while):
create-react-native-app weather --scripts-version 1.14.0
27
https://github.com/expo/xde
28
https://docs.expo.io/versions/latest/workflow/exp-cli
Getting Started with React Native 13
Importantly, we specify the --scripts-version as 1.14.0. We’ll talk about this in a moment.
We’ll then navigate to that directory and boot the app:
cd weather
yarn start
With the pacakger running, we can continue to scan the QR code with an Android device or send a
link directly to our iOS device using the s hotkey. It is important to remember that our device needs
to be connected to the same local network as our computer in order for this to work.
Right now, viewing the app shows our starting point:
Application
Getting Started with React Native 14
Running on a simulator
As we mentioned, using the Expo client app allows us to run our application without using
native tooling (Xcode for iOS, or Android Studio for Android).
However if we happen to have the required build tools we can still run our application in a
virtual device or simulator:
With a Mac, yarn run ios will start the development server and run the application
in an iOS simulator. We can also start the packager separately with yarn start and
press i to open the simulator.
With the required Android tools
29
, yarn run android will start the application in an
Android emulator. Similarly, pressing a when the React Native packager is running
will also boot up the emulator.
Running an application using an emulator/simulator can be useful to test on different devices
and screen sizes. It can also be quicker to update and test code changes on a virtual device.
However, it’s important to run your application on an actual device at some point in order
to get a better idea of how exactly it looks and feels.
By default, CRNA comes with live reload enabled. This means if you edit and save any file, the
application on your mobile device will automatically reload. Moreover, any build errors and logs
will be displayed directly in the terminal.
Let’s see what the directory structure of our app looks like. Open up a new terminal window.
Navigate to this app:
cd weather
And then run ls -a to see all the contents of the directory:
ls -a
If you’re using PowerShell or another non-Unix shell, you can just run ls.
Although your output will look slightly different based on your operating system, you should see
all the files in your directory listed:
29
https://facebook.github.io/react-native/docs/getting-started.html
Getting Started with React Native 15
├── node_modules/
├── .babelrc
├── .flowconfig
├── .gitignore
├── .watchmanconfig
├── App.js
├── app.json
├── App.test.js
├── package.json
├── README.md
└── yarn.lock
Let’s go through each of these files:
node_modules/ contains all third party packages in our application. Any new dependencies and
development dependencies go here.
.babelrc allows us to define presets and plugins for configuring Babel
30
. As we mentioned
previously, Babel is a transpiler that compiles newer experimental JavaScript into older versions
so that it stays compatible with different platforms.
.flowconfig allows us to configure Flow
31
, a static type checker for JavaScript. Flow is part
of the React Native toolchain and this file is included automatically in any React Native
application. We won’t be using Flow in this chapter but we will explore prop validations briefly
using prop-types.
.gitignore is where we specify which files should be ignored by Git. We can see that both the
node_modules/ and .expo/ directories are already included.
.watchmanconfig defines configurations for Watchman.
App.js is where our application code lives.
app.json is a configuration file that allows us to add information about our Expo app. The list
of properties that can be included in this file is listed in the documentation
32
.
App.test.js is included as a sample test file and contains a single test. CRNA is packaged with
Jest
33
as its testing platform. We’ll go into detail about unit testing React Native applications
in the “Testing” chapter.
package.json is where we provide information of the application to our package manager as
well as specify all our project dependencies.
README.md is a markdown file commonly used to provide a description of a project.
yarn.lock is where yarn keeps a record of the versions of each dependency installed.
package.json
Let’s take a closer look at the generated package.json file:
30
https://babeljs.io/
31
https://flow.org/
32
https://docs.expo.io/versions/v18.0.0/guides/configuration.html
33
https://facebook.github.io/jest/
Getting Started with React Native 16
1 {
2 "name": "weather",
3 "version": "0.1.0",
4 "private": true,
5 "devDependencies": {
6 "react-native-scripts": "1.14.0",
7 "jest-expo": "~27.0.0",
8 "react-test-renderer": "16.3.1"
9 },
10 "main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
11 "scripts": {
12 "start": "react-native-scripts start",
13 "eject": "react-native-scripts eject",
14 "android": "react-native-scripts android",
15 "ios": "react-native-scripts ios",
16 "test": "jest"
17 },
18 "jest": {
19 "preset": "jest-expo"
20 },
21 "dependencies": {
22 "expo": "^27.0.1",
23 "react": "16.3.1",
24 "react-native": "~0.55.2"
25 }
26 }
The name and version properties are always required. The dependencies and devDependencies
define our application and development dependencies respectively.
Notice there are three devDependencies:
react-native-scripts
jest-expo
react-test-renderer
The last two packages are related to testing. While CRNA is the tool that initializes our project,
the package react-native-scripts is the engine that runs our React Native app while in
development. When we specified the --scripts-version as 1.14.0 above, we were referring to
this package.
In our package.json, the scripts object defines all our script commands. These commands are all
handled by the react-native-scripts package. The commands yarn run start, yarn run android,
and yarn run ios allow us to start our application development server and/or run on a virtual device
or simulator. The scripts object also contains two other commands:
Getting Started with React Native 17
yarn test runs all the Jest tests in our application
yarn run eject starts the process of ejecting our application from the CRNA toolchain. As
we mentioned earlier, this can be necessary if we need to include a React Native library that
contains native code or if we need to write native code ourselves.
The utils/ directory
If you look at the book’s sample code, you’ll note that every application has a utils/ directory. This
directory contains helper functions that the application will use. You don’t need to concern yourself
with the details of these functions as they’re not relevant to the chapter’s core concepts.
When we reach the point in the application’s development where we need to use a utility provided
by utils/, we’ll remind you to copy over that folder from the sample code. You can also do this
immediately after initializing each project.
Components
With newer versions of JavaScript, we can define objects with properties using classes. React
Native lets us use this syntax to create components. Let’s take a look at a visual breakdown of the
components in our application:
Getting Started with React Native 18
Component Structure
We have an App component that represents the entire screen and contains the weather information
displayed to the use. Inside of this component, we have a SearchInput component that allows us to
search for different cities.
App
App is the first component created with a default CRNA application. Let’s take a look at its file:
Getting Started with React Native 19
weather/1/App.js
1 import React from 'react';
2 import { StyleSheet, Text, View } from 'react-native';
3
4 export default class App extends React.Component {
5 render() {
6 return (
7 <View style={styles.container}>
8 <Text>Open up App.js to start working on your app!</Text>
9 <Text>Changes you make will automatically reload.</Text>
10 <Text>Shake your phone to open the developer menu.</Text>
11 </View>
12 );
13 }
14 }
15
16 const styles = StyleSheet.create({
17 container: {
18 flex: 1,
19 backgroundColor: '#fff',
20 alignItems: 'center',
21 justifyContent: 'center',
22 },
23 });
Notice how we have a class defined in our file named App that extends React.Component. Using
extends allows us to declare a class as a subclass of another class. In here, we’ve defined App as
a subclass of React.Component. This is how we specify a specific class to be a component in our
application.
If you’d like to learn more about how classes work in JavaScript, refer to our Appendix.
We can also attach methods as properties to classes, and the same applies to component classes in
React Native. We can see we already have one for this component, the render method:
Getting Started with React Native 20
weather/1/App.js
5 render() {
6 return (
7 <View style={styles.container}>
8 <Text>Open up App.js to start working on your app!</Text>
9 <Text>Changes you make will automatically reload.</Text>
10 <Text>Shake your phone to open the developer menu.</Text>
11 </View>
12 );
13 }
What we see on our device when launching our device matches what we see described in this
method. The render() method is the only required method for a React Native component. React
Native uses the return value from this method to determine what to render for the component.
When we use React Native, we represent different parts of our application as components. This
means we can build our app using different reusable pieces of logic with each piece displaying a
specific part of our UI. Let’s break down what we already have in terms of components:
Our entire application is rendered with App as our top-level component. Although created
automatically as part of setting up a new CRNA project, this component is a custom component
responsible for rendering what we need in our application.
The View component is used as a layout container.
Within View, we use the Text component to display lines of text in our application. Unlike App,
both View and Text are built-in React Native components that are imported and used in our
custom component.
We can see that our App component uses and returns an HTML-like structure. This is JSX, which is
an extension of JavaScript that allows us to use an XML-like syntax to define our UI structure.
JSX
When we build an application with React Native, components ultimately render native views which
are displayed on our device. As such, the render() method of a component needs to describe how
the view should be represented. In other words, React Native allows us to describe a component’s
iOS and Android representation in JavaScript.
JSX was created to make the JavaScript representation of components easier to understand. It allows
us to structure components and show their hierarchy visually in markup. Consider this JSX snippet:
Getting Started with React Native 21
<View>
<Text style={{ color: 'red' }}>
Hello, friend! I am a basic React Native component.
</Text>
</View>
In here, we’ve nested a Text component within a View component. Notice how we use braces ({})
around an object ({ color: 'red' }) to set the style property value for Text. In JSX, braces are a
delimiter, signaling to JSX that what resides in-between the braces is a JavaScript expression. The
other delimiter is using quotes for strings, like this:
<TextInput placeholder="This is a string" />
Even though the JSX above might look similar to HTML, it is actually just compiled into
JavaScript function calls (ex: React.createElement(View)). For this reason, we need to
import React at the top of any file that contains JSX. You can refer to the Appendix for
more detail.
During runtime React Native takes care of rendering the actual native UI for each compo-
nent.
Props
We use the imported Text component to wrap each line of text output for our App component:
<Text>Open up App.js to start working on your app!</Text>
And we use the imported View component to wrap all the Text components:
<View style={styles.container}>
...
</View>
Props allow us to pass parameters to components to customize their features. Here, View is used
to layout the entire content of the screen. We only have a single prop attached, style, that allows
us to pass in style parameters to adjust how our View component is rendered on our devices. Each
built-in component provided by React Native has its own set of valid props that we can use for
customization.
Getting Started with React Native 22
If you’re familiar with HTML, it’s very similar. For example, in HTML, say you wanted to insert an
image named image.png. You’d specify an img tag with a src attribute like this:
<img src="path/to/image.png">
To give you an idea of the similarity, in React Native we can include images using the Image
component. We specify the location using the source prop:
<Image source={require('./image.png')}>
We’ll cover images in greater detail later.
Like our View component, many components in React Native accept a style prop. Styling is a large
topic that we explore throughout this book. However, we can take a look at our styles object at the
bottom of App.js and get an idea of how it works:
weather/1/App.js
16 const styles = StyleSheet.create({
17 container: {
18 flex: 1,
19 backgroundColor: '#fff',
20 alignItems: 'center',
21 justifyContent: 'center',
22 },
23 });
Web developers may recognize that this looks like CSS (Cascading Style Sheets) which is used to
style web pages. It’s important to note that styling in React Native does not use CSS. However, React
Native borrows a lot of styling nomenclature from web development. Here, we specify that the text
should be centered and that the background color should be white (#fff).
If you’ve used CSS before, you’ll find styling in React Native very familiar. If not, don’t
worry! It’s easy to get the hang of it.
Specifically, styles.container has the attributes flex, alignItems and justifyContent.
These are used to position the
View
in the center of the screen. React Native uses
flexbox
to
layout and align items consistently on different device sizes. We’ll go into more detail about
how exactly flexbox works in later chapters.
Getting Started with React Native 23
To build our weather app, we’ll start with layout and styling. Once we have some of the essence of
our weather app in place we can begin to explore strategies for managing data.
As we saw in the completed version of the app, we want our app to display the city, temperature,
and weather conditions as separate text fields. Although we’ll eventually interface with a weather
API in order to retrieve actual data, we’ll begin with hard-coding these values.
The completed app
Adding styles
To get a better handle on styling, let’s try adding an object with a color attribute to one of the text
fields:
<View style={styles.container}>
<Text style={{ color: 'red' }}>
Open up App.js to start working on your app!
</Text>
<Text>Changes you make will automatically reload.</Text>
<Text>Shake your phone to open the developer menu.</Text>
</View>
Getting Started with React Native 24
Note that the outer-most set of brackets above are delimiters enclosing our JavaScript
statement. Inside of the delimiters is a JavaScript object. In React Native, if the object is
small enough it’s common to just write it all on one line.
However, the double brackets ({{}}) might be confusing. Here’s another way of writing the
same component:
const style = { color: 'red' };
return (
<View style={styles.container}>
<Text style={style}>
Open up App.js to start working on your app!
</Text>
Save App.js. We can see our style applied once the application reloads:
Getting Started with React Native 25
As we mentioned previously, live reload is enabled by default in Expo. This means that with
any change to the code, the application will reload immediately. If you happen to not see any
changes reflected as soon as you save the file, you may have to check to see if this is enabled.
The documentation
34
explains how to open up the developer menu and enable/disable the
feature.
Although we can style our entire component this way, a lot of inline styles (or style attributes defined
directly within the delimeter of the style prop) used in a component can make things harder to read
and digest.
We can solve this by leveraging React Native’s Stylesheet API to separate our styles from our
component. With Stylesheet, we can create styles with attributes similar to CSS stylesheets. We
can see that Stylesheet is already imported at the top of the file. It’s used to declare our first style,
styles.container, which we use for View. We can add a new style called red to our styles:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
red: {
color: 'red',
},
});
We’ll then have Text use this style:
<View style={styles.container}>
<Text style={styles.red}>
Open up App.js to start working on your app!
</Text>
<Text>Changes you make will automatically reload.</Text>
<Text>Shake your phone to open the developer menu.</Text>
</View>
If we save our file and take a look at our app, we can see that the end result is the same.
Now let’s add some appropriate styles and text fields in order to display some weather data for a
location. To add multiple styles to a single component, we can pass in an array of styles:
34
https://docs.expo.io/versions/latest/guides/up-and-running.html#cant-see-your-changes
Getting Started with React Native 26
weather/2/App.js
14 <Text style={[styles.largeText, styles.textStyle]}>San Francisco</Text>
15 <Text style={[styles.smallText, styles.textStyle]}>Light Cloud</Text>
16 <Text style={[styles.largeText, styles.textStyle]}>24°</Text>
It is important to mention that when passing an array, the styles at the end of the array take
precedence over earlier styles, in case of any repeated attributes. We can see that we’re referencing
three new styles; textStyle, smallText, and largeText. Let’s define these within our styles object:
weather/2/App.js
29 const styles = StyleSheet.create({
30 container: {
31 flex: 1,
32 backgroundColor: '#fff',
33 alignItems: 'center',
34 justifyContent: 'center',
35 },
36 textStyle: {
37 textAlign: 'center',
38 fontFamily: Platform.OS === 'ios' ? 'AvenirNext-Regular' : 'Roboto',
39 },
40 largeText: {
41 fontSize: 44,
42 },
43 smallText: {
44 fontSize: 18,
45 },
textStyle specifies an alignment (center) as well as the fontFamily. Notice how we use
Platform to define platform specific fonts for both iOS and Android. We do this because both
operating systems provide a different set of native fonts.
smallText and largeText both specify different font sizes.
Platform is a built-in React Native API. We’ll need to make sure to import it:
Getting Started with React Native 27
weather/2/App.js
1 import React from 'react';
2 import {
3 StyleSheet,
4 Text,
5 KeyboardAvoidingView,
6 Platform,
7 TextInput,
8 } from 'react-native';
Let’s take a look at our application now:
Styled Text
Platform specific properties
The Platform API allows us to conditionally apply different styles or properties in our component
based on the device’s operating system. The OS attribute of the object returns either iOS or android
depending on the user’s device.
Getting Started with React Native 28
Although this is a relatively simple way to apply different properties in our application based on
the user’s device, there may be scenarios where we may want our component to be substantially
different between operating systems.
We can also use the Platform.select method that takes the operating system as keys within an
object and returns the correct result based on the device:
1 textStyle: {
2 textAlign: 'center',
3 ...Platform.select({
4 ios: {
5 fontFamily: 'AvenirNext-Regular',
6 },
7 android: {
8 fontFamily: 'Roboto',
9 },
10 }),
11 },
Separate files
Instead of applying conditional checks using Platform.OS a number times throughout the entire
component file, we can also leverage the use of platform specific files instead. We can create
two separate files to represent the same component each with a different extension: .ios.js and
.android.js. If both files export the same component class name, the React Native packager knows
to choose the right file based on the path extension. We’ll dive deeper into platform specific
differences later in this book.
Text input
We now have text fields that display the location, weather condition, and temperature. The next
thing we need to do is provide some sort of input to allow the user to search for a specific city.
Again, we’ll continue using hardcoded data for now. We’ll only begin using an API for real data
once we have all of our components in place.
React Native provides a built-in TextInput component that we can import into our component
that allows us to accept user input. Let’s include it within our View container underneath the Text
components (make sure to import it as well!):
Getting Started with React Native 29
weather/2/App.js
<Text style={[styles.largeText, styles.textStyle]}>San Francisco</Text>
<Text style={[styles.smallText, styles.textStyle]}>Light Cloud</Text>
<Text style={[styles.largeText, styles.textStyle]}>24°</Text>
<TextInput
autoCorrect={false}
placeholder="Search any city"
placeholderTextColor="white"
style={styles.textInput}
clearButtonMode="always"
/>
There are a number of props associated with TextInput that we can use. We’ll cover the basics
here but go into more detail about them in the “Core Components” chapter. Here we’re specifying
a placeholder, its color, as well as a style for the component itself. Let’s create its style object,
textInput, underneath our other styles:
weather/2/App.js
smallText: {
fontSize: 18,
},
textInput: {
backgroundColor: '#666',
color: 'white',
height: 40,
width: 300,
marginTop: 20,
marginHorizontal: 20,
paddingHorizontal: 10,
alignSelf: 'center',
},
As we mentioned previously, all the attributes that we provide styles with in React Native are
extremely similar to how we would apply them using CSS. Now let’s take a look at our application:
Getting Started with React Native 30
Text Input
We can see that the text input has a default underline on Android. We’ll go over how to remove this
in a bit.
We’ve also specified the clearButtonMode prop to be always. This shows a button on the right side
of the input field when characters are inserted that allows us to clear the text. This is only available
on iOS.
Text Input Clear Button
We can now type into the input field!
Getting Started with React Native 31
If you’re using the iOS simulator, you can connect your hardware keyboard and use that with
any input field. This can be done with Shift + ⌘ + K or going to Hardware -> Keyboard ->
Connect Hardware Keyboard
With this enabled, the software keyboard may not show by default. You can toggle this by
pressing ⌘ + K or going to Hardware -> Keyboard -> Toggle Software Keyboard
Now every time you click an input field, the software keyboard will display exactly how it
would if you were using a real device and you can type using your hardware keyboard.
However one thing you may have noticed is that when you focus on the input field with a tap, the
keyboard pops up and covers it on Android and comes quite close on iOS:
Keyboard
Since the virtual keyboard can cover roughly half the device screen, this is a common prob-
lem that occurs when using text inputs in an application. Fortunately, React Native includes
KeyboardAvoidingView, a component that solves this problem by allowing us to adjust where other
components render in relation to the virtual keyboard. Let’s import and use this component instead
of View:
Getting Started with React Native 32
weather/2/App.js
render() {
return (
<KeyboardAvoidingView style={styles.container} behavior="padding">
<Text style={[styles.largeText, styles.textStyle]}>San Francisco</Text>
<Text style={[styles.smallText, styles.textStyle]}>Light Cloud</Text>
<Text style={[styles.largeText, styles.textStyle]}>24°</Text>
<TextInput
autoCorrect={false}
placeholder="Search any city"
placeholderTextColor="white"
style={styles.textInput}
clearButtonMode="always"
/>
</KeyboardAvoidingView>
);
}
Notice that KeyboardAvoidingView accepts a behavior prop with which we can customize how the
keyboard adjusts. It can change its height, position or bottom padding in relation to the position of
the virtual keyboard. Here, we’ve specified padding.
Now tapping the text input will shift our component text and input fields out of the way of the
software keyboard.
Getting Started with React Native 33
Keyboard Avoiding View
Custom components
So far, we’ve explored how to add styling into our application, and we’ve included some built-in
components into our main App component. We use View as our component container and import
Text and TextInput components in order to display hardcoded weather data as well as an input
field for the user to change locations.
It’s important to re-iterate that React Native is component-driven. We’re already representing our
application in terms of components that describe different parts of our UI without too much effort,
and this is because React Native provides a number of different built-in components that you can
use immediately to shape and structure your application.
However, as our application begins to grow, it’s important to begin thinking of how it can further
be broken down into smaller and simpler chunks. We can do this by creating custom components
that contain a small subset of our UI that we feel fits better into a separate, distinct component
file. This is useful in order to allow us to further split parts of our application into something more
manageable, reusable and testable.
Although our application in its current state isn’t extremely large or unmanageable, there’s still some
room for improvement. The first way we can refactor our component is to move our TextInput into
Getting Started with React Native 34
a separate component to hide its implementation details from the main App component. Let’s create
a components directory in the root of the application with the following file:
├── components/
- SearchInput.js
All the custom components we create that we use in our main App component will live inside this
directory. For more advanced apps, we might create directories within components to categorize
them more specifically. Since this app is pretty simple, let’s use a flat components directory.
The SearchInput will be our first custom component so let’s move all of our code for TextInput
from App.js to SearchInput.js:
weather/3/components/SearchInput.js
1 import React from 'react';
2 import { StyleSheet, TextInput, View } from 'react-native';
3
4 export default class SearchInput extends React.Component {
5 render() {
6 return (
7 <View style={styles.container}>
8 <TextInput
9 autoCorrect={false}
10 placeholder={this.props.placeholder}
11 placeholderTextColor="white"
12 underlineColorAndroid="transparent"
13 style={styles.textInput}
14 clearButtonMode="always"
15 />
16 </View>
17 );
18 }
19 }
20
21 const styles = StyleSheet.create({
22 container: {
23 height: 40,
24 marginTop: 20,
25 backgroundColor: '#666',
26 marginHorizontal: 40,
27 paddingHorizontal: 10,
28 borderRadius: 5,
29 },
Getting Started with React Native 35
30 textInput: {
31 flex: 1,
32 color: 'white',
33 },
34 });
Let’s break down what this file contains:
We export a component named SearchInput.
This component accepts a placeholder prop.
This component returns a React Native TextInput with a few of its properties specified
wrapped within a View. We’ve applied the appropriate styles to our view container including
a borderRadius. We also added underlineColorAndroid="transparent" to remove the dark
underline that shows by default on Android.
this is a special keyword in JavaScript. The details about this are a bit nuanced, but for the
purposes of the majority of this book, this will be bound to the React Native component
class. So, when we write this.props inside the component, we’re accessing the props
property on the component. When we diverge from this rule in later sections, we’ll point it
out.
For more details on this, check out this page on MDN
35
.
Custom props
As you may recall, in App.js we set the placeholder prop for TextInput to “Search any city. That
renders the text input with a placeholder:
For SearchInput, we could hardcode a string again for placeholder. But what if we wanted to add
a search input elsewhere in our application? It would be nice if placeholder was customizable.
Earlier in this chapter, we explored how we can use props with a number of built-in components in
order to customize their features. We can also create props for custom components that we build as
well.
That’s what we do here in SearchInput. The component accepts the prop placeholder. In turn,
SearchInput uses this value to set the placeholder prop on TextInput.
35
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
Getting Started with React Native 36
The way data flows from parent to child in React Native is through props. When a parent renders
a child, it can send along props the child depends on. A component can access all its props through
the object this.props. If we decide to pass down the string "Type Here" as the placeholder prop,
the this.props object will look like this:
{ "placeholder": "Type Here" }
In here, we’ll set up App to render SearchInput which means that App is the parent of SearchInput.
Our parent component will be responsible for passing down the actual value of placeholder.
We’re getting somewhere interesting now. We’ve set up a custom SearchInput component and by
building it to accept a placeholder prop, we’re already setting it up to be configurable. Based on
what it receives, it can render any placeholder message that we’d like.
Importing components
In order to use SearchInput in App, we need to import the component first. We can remove the
TextInput logic from App.js and have App use SearchInput instead:
weather/3/App.js
import React from 'react';
import { StyleSheet, Text, KeyboardAvoidingView, Platform } from 'react-native';
import SearchInput from './components/SearchInput';
export default class App extends React.Component {
render() {
return (
<KeyboardAvoidingView style={styles.container} behavior="padding">
<Text style={[styles.largeText, styles.textStyle]}>San Francisco</Text>
<Text style={[styles.smallText, styles.textStyle]}>Light Cloud</Text>
<Text style={[styles.largeText, styles.textStyle]}>24°</Text>
<SearchInput placeholder="Search any city" />
</KeyboardAvoidingView>
);
}
}
By moving the entire TextInput details into a separate component called SearchInput, we’ve
made sure to not have any of its specific implementation details showing in the parent component
anymore. We can also remove the text input’s styling defined within the styles object.
Getting Started with React Native 37
There’s no specific answer to how often we should isolate different UI logic into separate custom
components. React Native was built in order to allow us to lay out our entire application in terms
of self-contained components, and that means we should separate parts of our application into
distinct units with custom functionality attached to them. This allows us to build a more manageable
application that’s easier to control and understand. We’ve isolated knowledge of our search input to
the component SearchInput and we’ll continue to isolate specific pieces of our app throughout this
chapter.
It’s common to separate your imports into two groups: imports from dependencies, and
imports from other files in your project. That’s why we put a blank line above SearchInput.
This comes down to personal style preference.
Background image
As we saw in the photo of the completed version of the app at the beginning of this chapter, we can
make our application more visually appealing by displaying a background image that represents the
current weather condition.
In this book’s sample code, we’ve included a number of images for various weather conditions. If you
inspect the weather/assets directory, you’ll find images like clear.png, hail.png, and showers.png.
If you’re following along, copy these two folders over from the sample code into your project:
1. weather/assets
2. weather/utils
We mentioned earlier that we’ve included a utils/ folder for each project in the book’s
sample code. This folder contains helper functions that we’ll use below.
If you’re on macOS or Linux, you can use cp -r to copy directories:
cp -r weather/{assets,utils} ~/react-native-projects/weather/
With the assets and utils folders copied over, let’s update our App component:
Getting Started with React Native 38
weather/4/App.js
import React from 'react';
import {
StyleSheet,
View,
ImageBackground,
Text,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import getImageForWeather from './utils/getImageForWeather';
import SearchInput from './components/SearchInput';
export default class App extends React.Component {
render() {
return (
<KeyboardAvoidingView style={styles.container} behavior="padding">
<ImageBackground
source={getImageForWeather('Clear')}
style={styles.imageContainer}
imageStyle={styles.image}
>
<View style={styles.detailsContainer}>
<Text style={[styles.largeText, styles.textStyle]}>
San Francisco
</Text>
<Text style={[styles.smallText, styles.textStyle]}>
Light Cloud
</Text>
<Text style={[styles.largeText, styles.textStyle]}>24°</Text>
<SearchInput placeholder="Search any city" />
</View>
</ImageBackground>
</KeyboardAvoidingView>
);
}
}
In this component, we’re importing a getImageForWeather method from our utils directory which
Getting Started with React Native 39
returns a specific image from the assets directory depending on a weather type. For example,
getImageForWeather('Clear') returns the following image:
Feel free to peek into the implementation details of any function we use from the utils
directory to get a better idea of how it works.
We also import React Native’s built-in ImageBackground component. Let’s take a closer look at how
we’re making use of it in our render method:
weather/4/App.js
render() {
return (
<KeyboardAvoidingView style={styles.container} behavior="padding">
<ImageBackground
source={getImageForWeather('Clear')}
style={styles.imageContainer}
imageStyle={styles.image}
>
Conceptually, the ImageBackground component is a View with an Image nested within. The source
prop accepts an image location, which we’ve set to getImageForWeather('Clear'). We know this
Getting Started with React Native 40
will always return the image displayed above. ImageBackground also uses the prop style for styling
the View container and the prop imageStyle for styling the image itself. Let’s add two new styles
and modify the container style:
weather/4/App.js
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#34495E',
},
imageContainer: {
flex: 1,
},
image: {
flex: 1,
width: null,
height: null,
resizeMode: 'cover',
},
Defining component styles with a flex attribute mean that they will expand to take up any room
remaining in their parent container in relation to any sibling components. They share this space in
proportion to their defined flex values. Since ImageBackground is the only nested element within
KeyboardAvoidingView, setting imageContainer to flex: 1 means that this element will fill up
the entire space of its parent component. We’ve removed justifyContent and alignItems from
container so that the ImageBackground can take up the entire device screen.
We also used flex: 1 to style the actual image itself, image, to make sure it takes up the entire
space of its parent container. With images in particular, the component will fetch and use the actual
width and height of the source image by default. For this reason, we’ve also set its height and width
attribues to null so that the dimensions of the image fit the container instead. The resizeMode
attribute allows us to define how the image is resized when the Image element does not match its
actual dimensions. Setting this attribute to cover means that the image will scale uniformly until it
is equal to the size of the component.
The “Core Components” chapter will dive deeper into how flexbox, layout, and the Image
component work in React Native
We also wrapped all of our Text elements and SearchInput within a view container styled with
detailsContainer:
Getting Started with React Native 41
weather/4/App.js
<View style={styles.detailsContainer}>
<Text style={[styles.largeText, styles.textStyle]}>
San Francisco
</Text>
<Text style={[styles.smallText, styles.textStyle]}>
Light Cloud
</Text>
<Text style={[styles.largeText, styles.textStyle]}>24°</Text>
<SearchInput placeholder="Search any city" />
</View>
Now let’s set up its style:
weather/4/App.js
detailsContainer: {
flex: 1,
justifyContent: 'center',
backgroundColor: 'rgba(0,0,0,0.2)',
paddingHorizontal: 20,
},
Here, we’re ensuring the container within ImageBackground also fills up the entire space of its
parent component as well as have its items aligned at the center of the screen. We also add a semi-
transparent overlay to our image by setting the backgroundColor of this component.
The last thing we’ll need to do here is change our Text elements to white instead of black to show
more clearly with a background image:
weather/4/App.js
textStyle: {
textAlign: 'center',
fontFamily: Platform.OS === 'ios' ? 'AvenirNext-Regular' : 'Roboto',
color: 'white',
},
Try it out
Save the file and take a look at our app. We should now see the background image displayed!
Getting Started with React Native 42
Modifying location
The steps we’ve taken so far are quite common when starting React Native applications. We hardcode
all our data, organize our app into components, and get an idea of the visual layout as well as how
it breaks down into components.
However, our app really isn’t very useful at this moment. If we take a look at our SearchInput
component for instance, we can type anything into the input field but nothing actually happens as
a result. We need to find a way to track changes made to the component and store that information
somewhere. In other words, we need some piece of mutable data that updates whenever the user
changes or submits the input field.
Instead of having SearchInput not actually manage any data that represents the text inputted by
the user, let’s pass in a prop for it called location to reflect what the user has inputted into the text
input field:
render() {
const location = 'San Francisco';
return (
<KeyboardAvoidingView style={styles.container} behavior="padding">
<ImageBackground
source={getImageForWeather('Clear')}
style={styles.imageContainer}
imageStyle={styles.image}
>
<View style={styles.detailsContainer}>
<Text style={[styles.largeText, styles.textStyle]}>{location}</Text>
<Text style={[styles.smallText, styles.textStyle]}>
Light Cloud
</Text>
<Text style={[styles.largeText, styles.textStyle]}>24°</Text>
<SearchInput placeholder="Search any city" />
</View>
</ImageBackground>
</KeyboardAvoidingView>
The reason we want to pass in the property that contains our location data is we need a way for our
child component to modify that field and communicate back up to our container App component.
Notice how we’ve moved the static string for location into a separate constant which we pass down
to SearchInput. We’ve instantiated it as San Francisco so that it can show as the first location when
the user loads the application. The next thing we just need to do is make sure that this location
constant is updated when the user actually changes the field in SearchInput:
Getting Started with React Native 43
export default class SearchInput extends React.Component {
handleChangeText(newLocation) {
// We need to do something with newLocation
}
render() {
return (
<TextInput
autoCorrect={false}
placeholder={this.props.placeholder}
placeholderTextColor="white"
underlineColorAndroid="transparent"
style={styles.textInput}
clearButtonMode="always"
onChangeText={this.handleChangeText}
/>
);
}
}
So what did we just do? We’ve just added onChangeText as a new prop to our TextInput component.
Notice that we don’t pass in a specific object or property, but a function instead:
onChangeText={this.handleChangeText}
This method is invoked everytime the text within the input field is changed. A number of built-
in components provided by React Native include event-driven props which we can attach specific
methods to. We’ll explore more throughout this book.
With onChangeText, our TextInput returns the changed text as an argument which we’re attempting
to pass into a separate method called handleChangeText. Currently our method is blank and we’ll
explore how we can complete it in a bit.
In React Native, we need to pass in functions when we want to handle certain events related to the
component being referenced. For the TextInput component, onChangeText is set to fire every single
time the text within the input field has changed. We need to “listen” to this specific event in our child
component (TextInput) so that it can notify our parent component (SearchInput) to respond to this
event. To do this, we pass in a function that calls another function, or in other words, a callback.
This is a common pattern when building components which need to notify a parent component of
some event. Unfortunately with the way we’ve just set it up, it wouldn’t work in this example. This
is because the function handleChangeText has a different local scope than the component instance.
We can work around this by binding our function to the correct context of its this object.
Getting Started with React Native 44
<TextInput
placeholder={placeholder}
placeholderTextColor="white"
underlineColorAndroid="transparent"
style={styles.textInput}
clearButtonMode="always"
onChangeText={this.handleChangeText.bind(this)}
/>
Now this might seem okay for the current context, but it can quickly become unwieldy if we build
our components with bind statements in each event handler. One reason why is if we wanted to use
handleChangeText in multiple different sub-components for example, we would have to make sure
to bind it to the correct context every single time. To help solve this, we can take care of handling
our event using property initializers:
export default class SearchInput extends React.Component {
handleChangeText = (newLocation) => {
// We need to do something with newLocation
}
render() {
return (
<TextInput
autoCorrect={false}
placeholder={this.props.placeholder}
placeholderTextColor="white"
underlineColorAndroid="transparent"
style={styles.textInput}
clearButtonMode="always"
onChangeText={this.handleChangeText}
/>
);
}
}
This allows us to declare the member methods as arrow functions:
handleChangeText = (newLocation) => {
// We need to do something with newLocation
}
And we pass the method name to the prop and nothing more:
Getting Started with React Native 45
onChangeText={this.handleChangeText}
Property Initializers
Supported by Babel
36
, property initializers are still in the proposal phase and have not yet
been slated for adoption in future JavaScript versions. Although this pattern is used quite
often in many React and React Native applications, it is important to keep in mind that it is
still experimental syntax.
For more information on the different ways to handle events in React Native, refer to the
Appendix.
Now that we’ve set up our callback correctly, let’s modify handleChangeText to change our text
prop in order to change the data to match what the user is typing:
handleChangeText = (newLocation) => {
this.props.location = newLocation;
};
Let’s run our application and try typing into the TextInput field. You’ll immediately notice that the
first location that shows is San Francisco, so we know that the text prop is being passed down
successfully!
However, if we type anything into our TextInput, you’ll notice nothing happens. Changing the text
within the input field does not actually update the parent location property and from the way
we’ve designed our component logic, it looks like it should. This is because this.props, which is
referenced in SearchInput, is actually owned by App and not the child component, SearchInput.
A component’s props are immutable and create a one-way data pipeline from parent to children.
We have a bit of a problem. We need to find a way to:
store local data in our child component, SearchInput, that represents the value in the input
field
track changes to the search input field as it’s updated by the user
notify our parent component, App, whenever our location changes
This is where we can use a component’s state.
Storing local data
Let’s modify our SearchInput component once more. Currently the text input within the component
does nothing, so let’s add some local component state to control actual data. We can do this by adding
a constructor method to the component. We can then initialize the component’s state within this
method:
36
https://babeljs.io/docs/plugins/transform-class-properties/
Getting Started with React Native 46
weather/5/components/SearchInput.js
export default class SearchInput extends React.Component {
constructor(props) {
super(props);
this.state = {
text: '',
};
}
We can use the constructor method to to initialize our component-specific data, or state. We
do this here because this method fires before our component is mounted and rendered. Here, we
defined our state object to only contain a text property:
Remember, components in React Native are extended from
React.Component
to create
derived classes. super() is required in derived classes in order to reference this within
the constructor.
Much like how we can access the component’s props with this.props, we can access the compo-
nent’s state via this.state. For example if we wanted to output our state property in a single Text
component, we could do this:
export default class HiThere extends React.Component {
constructor(props) {
super(props);
this.state = {
text: 'Hi there!',
};
}
render() {
return <Text>{this.state.text}</Text>;
}
}
This component would now render 'Hi there!' since that’s how we defined our state.text
property in our constructor. For our current component however, our text property in state will
be used to define the text typed by the user into the input field. Let’s now modify our component’s
render method to allow for this:
Getting Started with React Native 47
weather/5/components/SearchInput.js
render() {
const { placeholder } = this.props;
const { text } = this.state;
return (
<View style={styles.container}>
<TextInput
autoCorrect={false}
value={text}
placeholder={placeholder}
placeholderTextColor="white"
underlineColorAndroid="transparent"
style={styles.textInput}
clearButtonMode="always"
onChangeText={this.handleChangeText}
onSubmitEditing={this.handleSubmitEditing}
/>
</View>
);
}
The first thing we did was destructure the component props and state objects:
weather/5/components/SearchInput.js
render() {
const { placeholder } = this.props;
const { text } = this.state;
Destructuring
Instead of using this.props.placeholder and this.state.text directy, we destructured
both objects at the beginning of our render method into individual variables (text and
placeholder). Please refer to the Appendix for more details on destructuring assignments.
We then make sure that the TextInput placholder prop is still accepting our props.placeholder
attribute. We also pass state.text to a value prop:
Getting Started with React Native 48
weather/5/components/SearchInput.js
<TextInput
autoCorrect={false}
value={text}
placeholder={placeholder}
placeholderTextColor="white"
underlineColorAndroid="transparent"
style={styles.textInput}
clearButtonMode="always"
onChangeText={this.handleChangeText}
onSubmitEditing={this.handleSubmitEditing}
/>
The value prop is responsible for the content showed in the input field. With this, we now know
whatever is displayed in input field will always represent our local state.
We’ve also attached two additional props to our component, onChangeText and onSubmitEditing
with methods we haven’t set up yet.
Tracking changes to input
Let’s take a look at how
onChangeText
can allow us to update our
state
every time the input field is
changed. As we just did previously, we’re attaching a method to the onChangeText prop of TextInput:
weather/5/components/SearchInput.js
onChangeText={this.handleChangeText}
Previously, we set up a handleChangeText method that modifies our location prop value when the
user changes the text within the input. We quickly realized that this didn’t work. This is because
props are immutable and are always “owned” by a component’s parent while state can be mutated
and is “owned” by the component itself. This is an extremely important pattern to remember while
building components with React Native.
This brings us to setState(), a method we can use to change our state correctly. Let’s make use of
this in our handleChangeText method which we can declare right underneath our constructor:
Getting Started with React Native 49
weather/5/components/SearchInput.js
export default class SearchInput extends React.Component {
constructor(props) {
super(props);
this.state = {
text: '',
};
}
handleChangeText = text => {
this.setState({ text });
};
Shorthand property names
With later versions of JavaScript, we can define objects using shorthand form where possible.
Our handleChangeText method can also be written in a more explicit syntax:
handleChangeText = (text) => {
this.setState({ text: text });
};
Please refer to the Appendix for a little more detail on this concept.
Now we might be tempted to update our state by using this.state.text = text, but this will
not work. For all state modifications after the initial state we’ve defined in our constructor,
React provides components with the method setState() to do this. In addition to mutating the
component’s state object, this method triggers the React component to re-render, which is essential
after the state changes.
It’s good practice to initialize components with empty” state as we’ve done in this component.
However, after our SearchInput component is initialized, we want to update the state for with data
the user types into the text input. This is why we use the text argument provided into our callback
method as part of the onChangeText prop and pass that into this.setState().
Never modify state outside of this.setState(). This function has important hooks around
state modification that we would be bypassing.
We discuss state management in detail throughout the book.
Getting Started with React Native 50
Notifying the parent component
So we’ve found a way to correctly store local state in our component that represents the text within
the search input and make sure that it updates as the user changes the value. We still need to do
one more thing which is to notify our parent App component when the user submits a new searched
value. This is why we’ve attached a method to the onSubmitEditing prop of TextInput:
weather/5/components/SearchInput.js
onSubmitEditing={this.handleSubmitEditing}
The idea here is we don’t necessarily want to communicate with our parent component everytime
the user changes the input field. That’s why onChangeText is purely responsible for storing the latest
typed input value into the local state of the component. Fortunately, the TextInput component has
an onSubmitEditing prop which fires when the user submits the field and not just changes it. This
happens specifically when the user presses the action button of the virtual keyboard in order to
submit their input. This is where we would want to notify our container component of the typed
user data. Let’s take a look at how we can set up the handleSubmitEditing function that we’re
passing in:
weather/5/components/SearchInput.js
handleSubmitEditing = () => {
const { onSubmit } = this.props;
const { text } = this.state;
if (!text) return;
onSubmit(text);
this.setState({ text: '' });
};
In here, we check if this.state.text is not blank (which means the user has typed something into
the field), and if that’s the case:
1. Run an onSubmit function obtained from the component’s props. We pass text as an
argument here.
2. Clear the text property in state using this.setState()
We’ve seen how this.props can be used to pass information down from a parent component to child
and we’ve also seen how built-in components such as TextInput can notify their parent component
through callbacks in some of their props. Similarly, we can create props in our custom components
to do the exact same thing. In here, we need SearchInput to communicate with the App component
Getting Started with React Native 51
whenever the user submits the input field. We do this because we want our parent component to
handle the event of the user typing and submitting a new city. This is why we have an onSubmit
prop here that gets fired.
The next thing we need to do is pass a method to the onSubmit prop of SearchInput in App and
handle the event:
weather/5/App.js
<SearchInput
placeholder="Search any city"
onSubmit={this.handleUpdateLocation}
/>
Let’s define local state for this component as well as the handleUpdateLocation method:
weather/5/App.js
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
location: 'San Francisco',
};
}
handleUpdateLocation = city => {
this.setState({
location: city,
});
};
We defined local state for this component with just a location property and have it set to San
Francisco. We do this to ensure that an initial location is shown when we reload our application.
We also included a handleUpdateLocation method that takes in a parameter to change our location
state. This method will fire everytime the user submits the search input field because we pass this
method as the onSubmit prop for SearchInput.
Since we actually have “living” location data represented by what the user submits in the input field,
we can now display it in our first Text element instead of a hardcoded string:
Getting Started with React Native 52
weather/5/App.js
render() {
const { location } = this.state;
return (
<KeyboardAvoidingView style={styles.container} behavior="padding">
<ImageBackground
source={getImageForWeather('Clear')}
style={styles.imageContainer}
imageStyle={styles.image}
>
<View style={styles.detailsContainer}>
<Text style={[styles.largeText, styles.textStyle]}>{location}</Text>
Try it out
If we type any city in the search input and press return, we’ll see the name of the city being displayed
immediately.
Component State
Getting Started with React Native 53
This shows that we’ve wired everything correctly!
We’ve sequenced each of our problems step by step and showed how props and state differ when
trying to pass and store component data. However, handleUpdateLocation doesn’t really get real
weather information and just updates the city name that’s being displayed. We’ll wire it up to get
actual weather data soon.
Architecting state
We may have already considered controlling all the location state within SearchInput and not
having to deal with passing information upwards to a container component. There’s no specific
answer to where each piece of state should live and it depends on the type of application we’re
building. This is a core concept of building React Native applications and tools like Redux
37
and
MobX
38
aim to simplify this even further by allowing you to manage the entire state of the
application in a single location. However, even when we decide to use state management libraries
such as these examples, we still need to spend time deciding on how we want to structure our state
logic.
In our current app, we need to have App know the location data in order to display correct weather
conditions. SearchInput doesn’t really need to store this information without actually passing it up
to the component that handles the logic. The motivation behind keeping SearchInput simple is that
we can leverage React’s component-driven paradigm. We can re-use it in various places across our
application whenever we need a search input.
We can think of SearchInput as a component that provides presentational markup and does not
manage any real application data. Such components accept props from parent components which
specify the data a presentational component should render. This parent container component also
specifies behavior. If the lower level presentational component has any interactivity like our
search input it calls a prop-function given to it by the parent. We’ll go into more detail about
this important pattern throughout this book.
Lifecycle methods
We’ve wired up how our components communicate with each other to have a new location displayed
immediately when the user submits the text input field. However, you’ll notice that the city shows a
blank string when the app first loads. We could instantiate it with the name of an actual city instead
but we know we want to be getting actual weather information eventually. Although we haven’t set
that up just yet, the asynchronous action to fetch actual weather data for a city will be happening in
the handleUpdateLocation. Therefore it makes sense to call this method when our component first
loads. One thing we might be tempted to try is firing this method in our constructor:
37
https://github.com/reactjs/redux
38
https://github.com/mobxjs/mobx
Getting Started with React Native 54
constructor(props) {
super(props);
this.state = {
location: '',
};
this.handleUpdateLocation('San Francisco');
}
However, firing off asynchronous requests in the constructor is typically an anti-pattern. This is
because the constructor is called before the component is first mounted. As such, this method
should usually only be used to initialize state and bind methods.
Instead, we can make use of one of React Native’s lifecycle methods. Like the name suggests, these
methods allow you to access specific points in the lifecycle of a component. The term lifecycle here
applies to how React Native instantiates, changes and destroys components. We can use lifecycle
hooks to do something when these functions are called during different phases of component
rendering.
The most common lifecycle method used is the one that allows us to set component data after the
component is mounted componentDidMount(). This method is commonly used to trigger network
requests to fetch data that the component would need. To understand when this method fires, let’s
add it to our component right after our constructor with a console.log:
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
location: 'San Francisco',
};
}
componentDidMount() {
console.log('Component has mounted!');
}
When we reload our application, we can see Component has mounted! outputted directly to our
terminal as soon as the component has mounted.
Getting Started with React Native 55
Debugging in React Native
If you’ve worked with JavaScript on the web, you may be familiar with using console.log,
console.warn or console.error to output messages to the browser’s console for debugging
purposes. Similarly, Expo allows us to use these methods to output logs to our terminal. For
more detail about viewing logs, you can refer to the documentation
39
.
Aside from logging, React Native also allows us to debug the JavaScript code in our app
using the Chrome Developer Tools. With Expo, we can do this by pressing Debug Remote JS
in the developer menu. You can refer to the documentation
40
to learn more.
Now let’s update it to fire handleUpdateLocation:
weather/6/App.js
componentDidMount() {
this.handleUpdateLocation('San Francisco');
}
With this, we can remove San Francisco as our default location in state and set it to an empty
string.
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
location: '',
};
}
Since we’re using componentDidMount, we should still see San Francisco populated in place of the
text field as soon as we reload the app.
Although componentDidMount() allows us to create event listeners and fetch network
requests right after the component has rendered for the first time, there are number of other
lifecycle methods that React Native provides. We’ll go through each of them throughout this
book.
39
https://docs.expo.io/versions/latest/guides/logging.html
40
https://docs.expo.io/versions/latest/guides/debugging.html
Getting Started with React Native 56
Networking
We’ve built all the components that make up the UI of our app and refined it to show a nice
background image for the user. As we mentioned previously, the approach we’ve taken so far is
a common pattern used when building brand new React Native applications. We first organized our
views using components and then introduced some state and state management.
However, nobody will find our app useful unless it’s actually connected to real data. When building
a new mobile app, chances are we’ll need to communicate with a server. Communicating with a
server is a crucial component of most mobile applications.
For the purpose of this application, we’ll use the MetaWeather
41
API to fetch real weather
information. MetaWeather is a weather data aggregator that calculates the most likely outcome
from predictions of different forecasters. They provide an API
42
that provides this information over
a set of different endpoints:
1. Location search (/api/location/search/) which allows us to search for a particular city
2. Location weather information (/api/location/{woeid}) which provides a 5 day forecast for a
certain location
3. Location day which provides (/api/location/{woeid}/{date}/) forecast history and informa-
tion for a particular day and location
WOEID, or Where On Earth ID
43
, is a location identifier that allows us find details about a
specific location. For more detail on how exactly the MetaWeather API works, feel free to
take a closer look at the documentation
44
.
Now that we have a basic understanding of how state and props control the flow of data between
different components, let’s move on to using this API to render real weather data. It’s possible to
put API calls directly in our component methods, but it’s usually a good idea to abstract that logic
away in its own file. In the utils directory, we’ve set up two separate API calls in api.js:
fetchLocationId returns an array of locations based on a search query
fetchWeather returns weather details about a specific location using a location identifier
known as Where On Earth ID
45
The combination of both calls will allow us to search for a city and retrieve its weather information.
Feel free to open the file and take a look at how these methods work if you’re interested.
41
https://www.metaweather.com/
42
https://www.metaweather.com/api/
43
https://developer.yahoo.com/geo/geoplanet/guide/concepts.html
44
https://www.metaweather.com/api/
45
https://developer.yahoo.com/geo/geoplanet/guide/concepts.html
Getting Started with React Native 57
Async Functions
Callbacks and Promises are two ways to define asynchronous code in JavaScript. Built on
top of promises, async functions are a newer syntax that allows us to define asynchronous
methods in a synchronous manner. Both methods we’ve set up in api.js use this syntax.
Although supported by Babel, it is still in draft proposal stage and will most likely be ratified
into a future JavaScript release. Here’s the MDN
46
resource if you happen to be interested
in learning more about this syntax further.
When building components that fetch information over the network, it’s inevitable that the user will
have to wait a certain period of time before the data is retreived. With most applications, it makes
sense to show a loading indicator of some sort so the user knows they have to wait a bit before they
can see the content. Fortunately, React Native provides a built-in ActivityIndicator component
that displays a circular loading spinner. Let’s update our root App component beginning with some
new imports:
weather/6/App.js
import React from 'react';
import {
StyleSheet,
View,
ImageBackground,
Text,
KeyboardAvoidingView,
Platform,
ActivityIndicator,
StatusBar,
} from 'react-native';
import { fetchLocationId, fetchWeather } from './utils/api';
import getImageForWeather from './utils/getImageForWeather';
import SearchInput from './components/SearchInput';
We’ve added the following imports:
ActivityIndicator is a built-in component that displays a circular loading spinner. We’ll use
it when data is being fetched from the network
fetchLocationId, fetchWeather are the methods for interacting with the weather API
StatusBar is a built-in component that allows us to modify the app status bar at the top of the
device
46
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
Getting Started with React Native 58
Now let’s make some changes to our component. We need to apply our network request logic and
store that information so that it can be easily displayed. We also need to make sure that a loading
indicator is shown while the request is firing. Let’s begin with updating our state:
weather/6/App.js
constructor(props) {
super(props);
this.state = {
loading: false,
error: false,
location: '',
temperature: 0,
weather: '',
};
}
We just expanded our state object to include loading, error, temperature, and weather in addition
to location. The three latter properties are data we’ll retreive from the API. The loading property
represents when a call is still being made (in order to show a loading icon) and error is used to store
the error message if our call fails or returns unusable information.
With setState, updates to our state can happen asynchronously. For this reason, the method accepts
a callback as an optional second parameter that allows us to define an action to fire after the state
is updated. Consider the following as an example:
export default class Example extends React.Component {
state = {
weather: '',
};
componentDidMount() {
this.setState({ weather: 'Clear' }, () => console.log(this.state));
}
}
// { weather: 'Clear' } is logged right after our component finishes mounting
We can apply this logic to the method responsible for interfacing with our external API: handleUpdateLocation:
Getting Started with React Native 59
weather/6/App.js
handleUpdateLocation = async city => {
if (!city) return;
this.setState({ loading: true }, async () => {
try {
const locationId = await fetchLocationId(city);
const { location, weather, temperature } = await fetchWeather(
locationId,
);
this.setState({
loading: false,
error: false,
location,
weather,
temperature,
});
} catch (e) {
this.setState({
loading: false,
error: true,
});
}
});
};
We’ve updated it to be an asynchronous function that uses setState to change our loading attribute
to true. We also pass in an asynchronous function as its second argument. In here, we first call
fetchLocationId with the user queried city (if present) and pass the location ID to fetchWeather to
return an object that contains the required information (location, weather, and temperature). Once
complete, our state is updated with the correct parameters. Moreover, if any of the calls happen to
error, the catch statement will update the error property in our state to true.
Now that we have our API logic in place, we’ll need to do a few things in the UI of our component:
We need to display a loading spinner only when our API calls have fired but not completed
We should show an error message if the user types in an incorrect address or our API call fails
We need to render the correct weather information for a certain location
Let’s take a look at how we can update our render() method to do this:
Getting Started with React Native 60
weather/6/App.js
render() {
const { loading, error, location, weather, temperature } = this.state;
return (
<KeyboardAvoidingView style={styles.container} behavior="padding">
<StatusBar barStyle="light-content" />
<ImageBackground
source={getImageForWeather(weather)}
style={styles.imageContainer}
imageStyle={styles.image}
>
<View style={styles.detailsContainer}>
<ActivityIndicator animating={loading} color="white" size="large" />
{!loading && (
<View>
{error && (
<Text style={[styles.smallText, styles.textStyle]}>
Could not load weather, please try a different city.
</Text>
)}
{!error && (
<View>
<Text style={[styles.largeText, styles.textStyle]}>
{location}
</Text>
<Text style={[styles.smallText, styles.textStyle]}>
{weather}
</Text>
<Text style={[styles.largeText, styles.textStyle]}>
{`${Math.round(temperature)}°`}
</Text>
</View>
)}
<SearchInput
placeholder="Search any city"
onSubmit={this.handleUpdateLocation}
/>
</View>
)}
Getting Started with React Native 61
</View>
</ImageBackground>
</KeyboardAvoidingView>
);
}
It might look like a lot is going on in the file, but let’s break it down piece by piece. We first included
our StatusBar component:
weather/6/App.js
<StatusBar barStyle="light-content" />
The StatusBar component allows us to customize the status bar of our application using a barStyle
prop that lets us change the color of the text within the bar. A value of light-content renders a
lighter color (white) and dark-content will change it to a darker color (dark-grey).
With Expo, we can also configure the status bar for Android by modifying app.json.
Expo defaults barStyle for Android to light-content and makes the background translu-
cent. Although this looks fine for our current application, you can remove the translucency
by providing a background color. Take a look at the documentation
47
for more details.
We then added ActivityIndicator along with assigning its color and size prop:
weather/6/App.js
<ActivityIndicator animating={loading} color="white" size="large" />
Notice how we’ve also included an animating prop which we’ve set to be our state.loading
attribute. This prop is responsible for showing or hiding the component entirely.
After that, we’ve included a curly brace container in our JSX:
47
https://docs.expo.io/versions/latest/guides/configuring-statusbar.html
Getting Started with React Native 62
weather/6/App.js
{!loading && (
<View>
{error && (
<Text style={[styles.smallText, styles.textStyle]}>
Could not load weather, please try a different city.
</Text>
)}
{!error && (
<View>
<Text style={[styles.largeText, styles.textStyle]}>
{location}
</Text>
<Text style={[styles.smallText, styles.textStyle]}>
{weather}
</Text>
<Text style={[styles.largeText, styles.textStyle]}>
{`${Math.round(temperature)}°`}
</Text>
</View>
)}
<SearchInput
placeholder="Search any city"
onSubmit={this.handleUpdateLocation}
/>
</View>
)}
</View>
We’ve previously seen how JSX allows us to embed JavaScript expressions within curly braces.
Fortunately, this lets us include operators as well, allowing us to conditionally render certain parts
of our UI. In here, !loading && <...> means that this statement will evaluate and display the
element if and only if loading is false. We can see we’ve pretty much wrapped most of the elements
that make up our component within here, and this makes sense since we don’t want to show any
text fields or the search input while the API call is being fetched.
Getting Started with React Native 63
Conditional Rendering
Using logical && operators within the render method is not the only way to conditionally
render parts of the component. At times, this approach can make it harder to read a
component file if a significant number of lines are being conditionally rendered.
If this happens, it might be a good idea to use helper methods. For example, our render
method can be rewritten following this pattern:
renderContent() {
const { error } = this.state;
return (
<View>
{error && <Text>Error</Text>}
{!error && this.renderInfo()}
</View>
);
}
renderInfo() {
const { info } = this.state;
return <Text>{info}</Text>;
}
render() {
const { loading } = this.state;
return (
<View>
<ActivityIndicator animating={loading} color="white" size="large" />
{!loading && this.renderContent()}
</View>
);
}
The React documentation
48
goes into more detail about this concept as well as explaining
even more ways to conditionally render parts of components. Ultimately, it depends on
preference on which pattern to use.
Now within the content that shows when the API call isn’t being fired, we still need to be able to
display an appropriate error message if there’s an issue. We can use the state.error attribute to
conditionally display text in this scenario:
48
https://reactjs.org/docs/conditional-rendering.html
Getting Started with React Native 64
weather/6/App.js
{error && (
<Text style={[styles.smallText, styles.textStyle]}>
Could not load weather, please try a different city.
</Text>
)}
{!error && (
<View>
<Text style={[styles.largeText, styles.textStyle]}>
{location}
</Text>
<Text style={[styles.smallText, styles.textStyle]}>
{weather}
</Text>
<Text style={[styles.largeText, styles.textStyle]}>
{`${Math.round(temperature)}°`}
</Text>
</View>
)}
<SearchInput
placeholder="Search any city"
onSubmit={this.handleUpdateLocation}
/>
Notice how we now display our state information (location, weather, and temperature) in our
Text elements instead of hard-coded values. For temperature, we’re making use of the JavaScript
Math object and its round() method to round the temperature to the nearest integer.
The last thing we also do is pass the dynamic weather attribute to ImageBackground instead of a
hardcoded Clear string:
weather/6/App.js
<ImageBackground
source={getImageForWeather(weather)}
style={styles.imageContainer}
imageStyle={styles.image}
>
Now if we run our application, typing a city into the input field will return its actual weather data!
Getting Started with React Native 65
We’ve pretty much finished connecting all the major points of our application by wiring in network
requests to retreive actual data. After slowly beginning with hardcoded data and building our
components that make up the building blocks of our UI, our application now works just as we
intended from the beginning of this chapter. The next few sections will explore some additional
enhancements to our code but won’t add any new functionality to our app.
PropTypes
With React Native, we can include validation functions using the prop-types library. This allows
us to specify and enforce the type of our component props and ensure that match what we expect
them to be. This can not only help us catch development errors sooner but also provide a layer of
documentation to the consumer of our components
We can add prop-types as a dependency:
yarn add prop-types
Now let’s take a look at how we can use PropTypes in SearchInput:
Getting Started with React Native 66
weather/6/components/SearchInput.js
SearchInput.propTypes = {
onSubmit: PropTypes.func.isRequired,
placeholder: PropTypes.string,
};
SearchInput.defaultProps = {
placeholder: '',
};
We’ve defined a propTypes object which instructs React to validate the props given to our
component. We’re specifying that onSubmit must be a function and placeholder must be a string.
We’ve also specified onSubmit to be required which means it has to be provided to out component
and is not optional.
We’ve left placeholder to be optional. For this, we’re making use of the defaultProps object. This
allows us to create our component and not specify placeholder if we don’t need to, defaultProps
will take care of providing it’s value in that case. It’s important to note that the value passed into
defaultProps also undergoes type-checking as well by the library.
Now what exactly happens when a prop’s type is not validated successfully? When a prop is passed
in with an invalid type or fails the propType validation, a warning is passed into the JavaScript
console. These warnings will only be shown in development mode, so if we accidentally deploy our
app into production with an improper use of a component, our users won’t see the warning.
Class properties
React Native includes class properties transformation
49
from Babel that allows us to simplify how
we define our component state, props, and propTypes. For example, we can update the constructor
in App.js to:
weather/App.js
state = {
loading: false,
error: false,
location: '',
temperature: 0,
weather: '',
};
This gets transpiled into the exact same result as using a constructor. Similarly, we can simplify
how we define our state in SearchInput:
49
https://babeljs.io/docs/plugins/transform-class-properties/
Getting Started with React Native 67
weather/components/SearchInput.js
state = {
text: '',
};
Moreover, we can also set propTypes and defaultProps using static properties in our class. In other
words, we can remove the object references in SearchInput and define a static method within the
class:
weather/components/SearchInput.js
static propTypes = {
onSubmit: PropTypes.func.isRequired,
placeholder: PropTypes.string,
};
static defaultProps = {
placeholder: '',
};
Using this pattern and leveraging class properties transform is purely syntactical sugar over defining
methods and objects separately and allows us to write in a cleaner, simpler syntax.
Summary
Congratulations, we’ve just built our very first React Native application and covered almost all of
the essentials needed to build a complete and fully functional mobile app. We began by exploring
each of the files generated as a result of starting a new project with CRNA and how Expo allows us
to run our application smoothly on our device without worrying about Xcode and Android Studio
set up. We then built out each of the components that make up our application using the built-in
components provided by React Native. While doing so, we dove into the fundamentals of React
Native understanding JSX, how to apply custom styling as well as understanding how to use props
and state to manage and control data. We moved on to more complex topics including lifecycle
methods and how to use external network calls to provide real content to our application. Finally,
we finished off with a brief look into how propTypes can add an additional layer of safety by adding
type validation to our application. The rest of this book will dive deeper into core concepts of React
Native and the concepts learned in this chapter will serve as the foundation for everything else in
the text.
So far, we’ve only scratched the surface of what React Native allows us to do. By knowing the
setup/development details like Expo and the core concepts of props, state, and components, you
already have the essentials of React Native development under your belt. As of now, you can already
build a wide variety of applications using the framework so go forth and build something amazing!
React Fundamentals
In the last chapter, we built our first React Native application. We explored how React applications
are organized by components. Using the key React concepts of state and props, we saw how data
is managed and how it flows between components. We also discussed other useful concepts, like
handling user input and fetching data from a remote API.
In this section, we’ll build another application step-by-step. We’ll dive even deeper into React’s
fundamentals. We’ll investigate a pattern that you can use to build React Native apps from scratch
and then put those steps to work to build a time-tracking application.
In this app, a user can add, delete, and modify various timers. Each timer corresponds to a different
task that the user would like to keep time for:
Time Tracking App
This app will have significantly more interactive capabilities than the one built in the last chapter.
As we’ll see, this will present us with some interesting challenges.
Getting started
This chapter assumes you’ve setup your system by following the steps at the beginning of the first
chapter.
As with all the chapters in this book, make sure you have the book’s sample code at the ready.
React Fundamentals 69
Previewing the app
Let’s begin by viewing the completed app. To try the completed app on your device:
On Android, you can scan this QR code using the Expo app:
QR Code
On iOS, you can navigate to the time-tracking/ directory within the sample code folder and
either preview it on the iOS simulator or send the link of the project URL to your device as we
explained in the previous chapter.
Play around with it to get a feel for all the functionality.
Breaking the app into components
Let’s start by breaking our app down into its components. As we noticed in our last project, visual
components usually map tightly to their respective React Native components. For example, we can
imagine that we’d want a Timer component for each timer:
React Fundamentals 70
Our application displays a list of timers and has a “+” icon at the top. We’re able to add new
timers to the list using this button. This “+” component is interesting because it has two distinct
representations. When the “+” button is pressed, the component changes into a form:
React Fundamentals 71
When the form is closed, the component changes back into a “+” button.
There are two approaches we could take. The first one is to have the parent component decide
whether or not to render a “+” component or a form component based on some piece of stateful
data. It could swap between the two children. However, this adds more responsibility to the parent
component. Since no other child components need this piece of information, it might make more
sense to have a new child component own the single responsibility of determining whether or not
to display a “+” button or a create timer form. We’ll call it ToggleableTimerForm. As a child, it can
either render the component TimerForm or the “+” button.
So, we’ve identified two components in addition to our root application component:
React Fundamentals 72
But the Timer component has a fair bit of functionality. As we saw in the completed version of the
app, each timer turns into a form when the user clicks “Edit”:
A single timer: Displaying time (left) vs. edit form (right)
In addition, timers delete themselves when “Remove” is pressed and have buttons for starting and
stopping. Do we need to break this up? And if so, how?
Displaying a timer and editing a timer are indeed two distinct UI components. They should be two
distinct React components. Like ToggleableTimerForm, we need some container component that
renders either the timer’s face or its edit form depending on if the timer is being edited.
React Fundamentals 73
We’ll call this EditableTimer. The child of EditableTimer will then be either a Timer component or
the edit form component. The form for creating and editing timers is very similar, so let’s assume
that we can use the component TimerForm in both contexts:
As for the other functionality of the timer, like the start and stop buttons, it’s a bit tough to determine
at this point whether or not they should be their own components. We can trust that the answers
will be more apparent after we’ve started writing some code and have a better idea of the general
structure of the components in our application.
So, we have our final component hierarchy, with some ambiguity around the final state of the timer
component:
React Fundamentals 74
App: Root container
EditableTimer: Displays either a timer or a timer’s edit form
* Timer: Displays a given timer
* TimerForm: Displays a given timer’s edit form
ToggleableTimerForm: Displays a form to create a new timer
* TimerForm: Displays a new timer’s create form
For all the buttons in the app, we’ll create and use a component called TimerButton.
7 step process
Now that we have a good understanding of the composition of our components, we’re ready to build
a static version of our app that only contains hardcoded data. As we noticed in the previous chapter,
many applications we build will require our top-level component to communicate with a server. In
these scenarios, the server will be the initial source of state, and React Native will render itself
according to the data the server provides. If our current app followed this pattern it would also
send updates to the server, like when a timer is started. However, for simplicity, in this chapter we’ll
render local state rather than communicating with a server.
React Fundamentals 75
It always simplifies things to start off with static components, as we did in the last chapter. The
static version of the app will not be interactive. Pressing buttons, for example, won’t do anything.
But this will enable us to lay the framework for the app, getting a clear idea of how the component
tree is organized.
Next, we can determine what the state should be for the app and in which component it should live.
At that point, we’ll have the data flow from parent to child in place. Then we can add inverse data
flow, propagating events from child to parent.
In fact, this follows from a handy process for developing a React Native app from scratch:
1. Break the app into components
2. Build a static version of the app
3. Determine what should be stateful
4. Determine in which component each piece of state should live
5. Hardcode initial states
6. Add inverse data flow
7. Add server communication (if present)
We followed this pattern in the last project:
1. Break the app into components
We looked at the desired UI and determined we wanted a custom SearchInput component.
2. Build a static version of the app
Our components started off without using state. Instead, we had our root App component pass down
location as a static prop to SearchInput.
3. Determine what should be stateful
In order for our application to become interactive, we had to be able to modify the search value of
the search input. The value submitted was our stateful location property.
4. Determine in which component each piece of state should live
Our root App component was responsible for managing the location, temperature, and weather
state parameters using React component class methods.
5. Hardcode initial state
We defined a hardcoded location value and passed it down to SearchInput as a custom prop.
6. Add inverse data flow
We defined the handleUpdateLocation function in our App container and passed it down in props
so that SearchInput could inform the parent of when our search input’s submit button is pressed.
7. Add server communication
React Fundamentals 76
We added server communication between our parent component and the MetaWeather API to
retrieve actual weather data.
These steps only serve as a guideline. You don’t necessarily have to follow it every time you build an
application, but you’ll likely internalize and become more accustomed to following this structure as
you build more applications. If steps in this process aren’t completely clear right now, don’t worry.
The purpose of this chapter is to familiarize yourself with this procedure.
We’ve already covered step (1) and have a good understanding of all of our components, except
for some uncertainty down at the Timer component. Step (2) is to build a static version of the app.
As in the last project, this amounts to defining React components, their hierarchy, and their HTML
representation. We avoid state for now.
Step 2: Build a static version of the app
Prepare the app
Before beginning, run the following commands in your terminal to create a new React Native app:
create-react-native-app time-tracking --scripts-version 1.14.0
cd time-tracking
yarn start
App
Let’s start off by writing our App component in the file App.js. We’ll begin with our imports:
time-tracking/1/App.js
import React from 'react';
import { StyleSheet, View, ScrollView, Text } from 'react-native';
import EditableTimer from './components/EditableTimer';
import ToggleableTimerForm from './components/ToggleableTimerForm';
After importing the core React Native components we’ll be using in App, we import EditableTimer
and ToggleableTimerForm. We’ll be implementing those shortly.
We’ll have our App component render both ToggleableTimerForm and a couple of EditableTimer
components. Because we’re building the static version of our app, we’ll manually set all the props:
React Fundamentals 77
time-tracking/1/App.js
export default class App extends React.Component {
render() {
return (
<View style={styles.appContainer}>
<View style={styles.titleContainer}>
<Text style={styles.title}>Timers</Text>
</View>
<ScrollView style={styles.timerList}>
<ToggleableTimerForm isOpen={false} />
<EditableTimer
id="1"
title="Mow the lawn"
project="House Chores"
elapsed="8986300"
isRunning
/>
<EditableTimer
id="2"
title="Bake squash"
project="Kitchen Chores"
elapsed="3890985"
editFormOpen
/>
</ScrollView>
</View>
);
}
}
At the top, we display a title (“Timers”) inside of a Text component. We’ll look at the styles object
in a moment.
After our title, we render the rest of the components in a ScrollView component. The built-in
ScrollView component in React Native is responsible for wrapping components within a scrolling
container.
We’re passing down one prop to ToggleableTimerForm: isOpen. This is used by the child component
to determine whether to render a “+” or TimerForm. When ToggleableTimerForm is “open” the form
is being displayed.
We also include two separate EditableTimer components within App. We’ll dig into each of these
props when we build the component. Notably, isRunning specifies whether the timer is running and
editFormOpen specifies whether EditableTimer should display the timer’s face or its edit form.
React Fundamentals 78
Note that we don’t explicitly set any values for the props isRunning on the first EditableTimer or
editFormOpen on the second:
time-tracking/1/App.js
<EditableTimer
id="1"
title="Mow the lawn"
project="House Chores"
elapsed="8986300"
isRunning
/>
<EditableTimer
id="2"
title="Bake squash"
project="Kitchen Chores"
elapsed="3890985"
editFormOpen
/>
This is a style for boolean props you’ll often encounter in React Native apps. When no explicit value
is passed, the prop defaults to true. So <ToggleableTimerForm isOpen /> will give the same result
as <ToggleableTimerForm isOpen={true}/>. Conversely, when a prop is absent it is undefined. This
means that for the first timer editFormOpen is “falsy.
ScrollView renders all of its components at once, even those not currently shown in the
screen.
Last, here are the styles we’re using:
time-tracking/1/App.js
const styles = StyleSheet.create({
appContainer: {
flex: 1,
},
titleContainer: {
paddingTop: 35,
paddingBottom: 15,
borderBottomWidth: 1,
borderBottomColor: '#D6D7DA',
},
title: {
fontSize: 18,
React Fundamentals 79
fontWeight: 'bold',
textAlign: 'center',
},
timerList: {
paddingBottom: 15,
},
});
We’re not going to focus on styles in this chapter so feel free to just copy over the styles object for
each component.
EditableTimer
With all of our child components, we’ll save their respective files within a components subdirectory.
Let’s create components/EditableTimer.js.
First, we’ll begin by implementing TimerForm and Timer. We’ll be creating those shortly:
time-tracking/1/components/EditableTimer.js
import React from 'react';
import TimerForm from './TimerForm';
import Timer from './Timer';
EditableTimer will either return a timer’s face (Timer) or a timer’s edit form (TimerForm) based on
the prop editFormOpen. We don’t anticipate this component will ever manage state.
So far, we’ve written React components as ES6 classes that extend React.Component. However,
there’s another way to declare React components: as functions.
Let’s see what that looks like:
time-tracking/1/components/EditableTimer.js
export default function EditableTimer({
id,
title,
project,
elapsed,
isRunning,
editFormOpen,
}) {
if (editFormOpen) {
return <TimerForm id={id} title={title} project={project} />;
React Fundamentals 80
}
return (
<Timer
id={id}
title={title}
project={project}
elapsed={elapsed}
isRunning={isRunning}
/>
);
}
EditableTimer is a regular JavaScript function. In React, we call components written this way state-
less functional components or functional components for short. While we can write EditableTimer
using either component style, it’s a perfect candidate to be written as a function.
Think of functional components as components that only need to implement the render() method.
They don’t manage state and don’t need any of React’s special lifecycle hooks.
Throughout this book, we’ll refer to the two different types as class components and
functional components.
Note that the props are passed in as the first argument to the function. We don’t use this when
working with functional components. Here, we use destructuring to extract all the props from the
props object.
The component’s render method switches on the prop editFormOpen. If true, we render a TimerForm.
Otherwise, we render Timer.
As we saw in App, this component receives six props. This component passes down the props id,
title and project to TimerForm. For Timer, we pass down all the timer attributes.
Benefits of functional components
Why would we want to use functional components? There are two main reasons:
First, using functional components where possible encourages developers to manage state in fewer
locations. This makes our programs easier to reason about.
Second, using functional components are a great way to create reusable components. Because
functional components need to have all their configuration passed from the outside, they are easy
to reuse across apps or projects.
A good rule of thumb is to use functional components as much as possible. If we don’t need any
lifecycle methods and can get away with only a render() function, using a functional component
is a great choice.
React Fundamentals 81
Note that React still allows us to set propTypes and defaultProps on functional components.
TimerForm
TimerForm will contain two TextInput fields for editing a timer’s title and project. We’ll also add
a pair of buttons at the bottom.
Like EditableTimer, we can write this component as a functional component:
time-tracking/1/components/TimerForm.js
import React from 'react';
import { StyleSheet, View, Text, TextInput } from 'react-native';
import TimerButton from './TimerButton';
export default function TimerForm({ id, title, project }) {
const submitText = id ? 'Update' : 'Create';
return (
<View style={styles.formContainer}>
<View style={styles.attributeContainer}>
<Text style={styles.textInputTitle}>Title</Text>
<View style={styles.textInputContainer}>
<TextInput
style={styles.textInput}
underlineColorAndroid="transparent"
defaultValue={title}
/>
</View>
</View>
<View style={styles.attributeContainer}>
<Text style={styles.textInputTitle}>Project</Text>
<View style={styles.textInputContainer}>
<TextInput
style={styles.textInput}
underlineColorAndroid="transparent"
defaultValue={project}
/>
</View>
</View>
<View style={styles.buttonGroup}>
React Fundamentals 82
<TimerButton small color="#21BA45" title={submitText} />
<TimerButton small color="#DB2828" title="Cancel" />
</View>
</View>
);
}
const styles = StyleSheet.create({
formContainer: {
backgroundColor: 'white',
borderColor: '#D6D7DA',
borderWidth: 2,
borderRadius: 10,
padding: 15,
margin: 15,
marginBottom: 0,
},
attributeContainer: {
marginVertical: 8,
},
textInputContainer: {
borderColor: '#D6D7DA',
borderRadius: 2,
borderWidth: 1,
marginBottom: 5,
},
textInput: {
height: 30,
padding: 5,
fontSize: 12,
},
textInputTitle: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 5,
},
buttonGroup: {
flexDirection: 'row',
justifyContent: 'space-between',
},
});
React Fundamentals 83
We wrap each of our form elements in a View container. Each input field has a label (“Title” and
“Project”) above a TextInput.
At the end of the component, we have a button group with two TimerButton instances. We’ll create
this component in a bit.
Let’s take a closer look at how we’ve set up TextInput for the timer’s title:
time-tracking/1/components/TimerForm.js
<TextInput
style={styles.textInput}
underlineColorAndroid="transparent"
defaultValue={title}
/>
And for the timer’s project:
time-tracking/1/components/TimerForm.js
<TextInput
style={styles.textInput}
underlineColorAndroid="transparent"
defaultValue={project}
/>
Aside from adding styles using the style prop, we’re also using the TextInput component’s
defaultValue property. When the form is used for editing as it is here, we want the fields to be
populated with the current title and project values for this timer. Using defaultValue initializes
these fields with the current values, as desired.
Later, we’ll use TimerForm again within ToggleableTimerForm for creating timers.
ToggleableTimerForm will not pass TimerForm the title or project props. We’ll use
defaultProps to default these values to empty strings.
At the beginning of the component function, before the return statement, we define the variable
submitText. This variable uses the presence of props.id to determine what text the submit button
at the bottom of the form should display. If id is present, we know we’re editing an existing timer,
so it displays “Update. Otherwise, it displays “Create.
With all of this logic in place, TimerForm is prepared to render a form for creating a new timer or
editing an existing one.
React Fundamentals 84
We used an expression with the ternary operator to set the value of submitText. The syntax
is:
condition ? expression1 : expression2
If the condition is true, the ternary expression evaluates to expression1. Otherwise, it
evaluates to expression2. In our example, the variable submitText is set to the result of
the ternary expression.
TimerButton
Now let’s set up a component that we can use for all the buttons in our application, TimerButton.
Again, we can write this as a functional component:
time-tracking/1/components/TimerButton.js
1 import { StyleSheet, Text, TouchableOpacity } from 'react-native';
2 import React from 'react';
3
4 export default function TimerButton({ color, title, small, onPress }) {
5 return (
6 <TouchableOpacity
7 style={[styles.button, { borderColor: color }]}
8 onPress={onPress}
9 >
10 <Text
11 style={[
12 styles.buttonText,
13 small ? styles.small : styles.large,
14 { color },
15 ]}
16 >
17 {title}
18 </Text>
19 </TouchableOpacity>
20 );
21 }
22
23 const styles = StyleSheet.create({
24 button: {
25 marginTop: 10,
26 minWidth: 100,
27 borderWidth: 2,
React Fundamentals 85
28 borderRadius: 3,
29 },
30 small: {
31 fontSize: 14,
32 padding: 5,
33 },
34 large: {
35 fontSize: 16,
36 padding: 10,
37 },
38 buttonText: {
39 textAlign: 'center',
40 fontWeight: 'bold',
41 },
42 title: {
43 fontSize: 14,
44 fontWeight: 'bold',
45 },
46 elapsedTime: {
47 fontSize: 18,
48 fontWeight: 'bold',
49 textAlign: 'center',
50 paddingVertical: 10,
51 },
52 });
React Native provides a built-in Button component, but it only allows for limited customization. For
this reason, we’re leveraging TouchableOpacity, which renders a wrapper to allow for components
to respond with opacity changes when pressed.
For easier customization, we’ve included color, title, and small as props that will allow us to
change how our button looks. The title prop is responsible for the button text while the color prop
changes the text and border colors. The small prop is a boolean prop passed in to render a smaller
button with slightly different styling.
Since we plan on using this component in multiple places in our app, we’ve defined an onPress prop
in order to fire a specific function that’s passed into our component when the button is pressed. We’re
not using it currently in TimerForm but we will as soon as we add actual data in our application.
TouchableOpacity accepts an activeOpacity prop that allows us to determine what the
opacity of the view should be when pressed. This defaults to a value of 0.2.
React Fundamentals 86
ToggleableTimerForm
Let’s turn our attention next to ToggleableTimerForm. Recall that this is a wrapper component
around TimerForm. It will display either a “+” or a TimerForm. Right now, it accepts a single prop,
isOpen, from its parent that instructs its behavior:
time-tracking/1/components/ToggleableTimerForm.js
import React from 'react';
import { StyleSheet, View } from 'react-native';
import TimerButton from './TimerButton';
import TimerForm from './TimerForm';
export default function ToggleableTimerForm({ isOpen }) {
return (
<View style={[styles.container, !isOpen && styles.buttonPadding]}>
{isOpen ? <TimerForm /> : <TimerButton title="+" color="black" />}
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingVertical: 10,
},
buttonPadding: {
paddingHorizontal: 15,
},
});
As noted earlier, TimerForm does not receive any props from ToggleableTimerForm. As such, its
title and project fields will be rendered empty.
We’re using a ternary operator again here to either return TimerForm or render a “+” button. You
could make a case that this should be its own component (say PlusButton) but at present we’ll keep
the code inside ToggleableTimerForm.
Timer
Time for the Timer component.
As with all projects in this book, the sample code for this project comes with a utils/ directory
that contains various functions that will aid in the construction of this app. We’ll be using one of
React Fundamentals 87
those functions now. If you haven’t already, go ahead and copy over time-tracking/utils/ from
the sample code to your project directory now.
With utils/ in place, let’s take a look at our first version of Timer:
time-tracking/1/components/Timer.js
import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { millisecondsToHuman } from '../utils/TimerUtils';
import TimerButton from './TimerButton';
export default function Timer({ title, project, elapsed }) {
const elapsedString = millisecondsToHuman(elapsed);
return (
<View style={styles.timerContainer}>
<Text style={styles.title}>{title}</Text>
<Text>{project}</Text>
<Text style={styles.elapsedTime}>{elapsedString}</Text>
<View style={styles.buttonGroup}>
<TimerButton color="blue" small title="Edit" />
<TimerButton color="blue" small title="Remove" />
</View>
<TimerButton color="#21BA45" title="Start" />
</View>
);
}
const styles = StyleSheet.create({
timerContainer: {
backgroundColor: 'white',
borderColor: '#d6d7da',
borderWidth: 2,
borderRadius: 10,
padding: 15,
margin: 15,
marginBottom: 0,
},
title: {
fontSize: 14,
fontWeight: 'bold',
},
elapsedTime: {
React Fundamentals 88
fontSize: 26,
fontWeight: 'bold',
textAlign: 'center',
paddingVertical: 15,
},
buttonGroup: {
flexDirection: 'row',
justifyContent: 'space-between',
},
});
The elapsed prop in this app is in milliseconds. This is the representation of the data that React will
keep. This is a good representation for machines, but we want to show our users a more human-
readable format.
We use a function defined in ./utils/TimerUtils, millisecondsToHuman(). You can pop open that
file if you’re curious about how it’s implemented. The string it renders is in the format ‘HH:MM:SS’.
Note that we could store elapsed in seconds as opposed to milliseconds, but JavaScript’s
time functionality is all in milliseconds. We keep elapsed consistent with this for simplicity.
Try it out
With all of our components laid out, let’s boot up the React Native packager to see our app so far:
React Fundamentals 89
Tweak some of the props and refresh to see the results. For example:
Flip the prop passed down to ToggleableTimerForm from false to true and see the timer form
render in the place of the “+” button:
React Fundamentals 90
Remove the editFormOpen prop in the second EditableTimer component within App and
witness the component flip the child it renders accordingly:
React Fundamentals 91
To review, our App component currently renders a ToggleableTimerForm component and two
EditableTimer components.
ToggleableTimerForm renders either a “+” or a TimerForm based on the prop isOpen.
EditableTimer renders either Timer or TimerForm based on the prop editFormOpen. Timer and
TimerForm are our app’s bottom-level components. They hold the majority of the screen’s UI. The
components above them are primarily concerned with orchestration.
So far, we’ve used hardcoded props to pass data around our app. But in order to enhance our app
with interactivity, we must evolve it from its static existence to a mutable one. As we saw in the last
chapter, in React we use state to accomplish this.
Step 3: Determine what should be stateful
Before introducing state, we need to determine what, exactly, should be stateful. Let’s start by
collecting all of the data that’s consumed by each component in our static app. In our static app,
data will be wherever we are defining or using props. We will then determine which of that data
should be stateful.
App
React Fundamentals 92
This declares two child components. It sets one prop, which is the isOpen boolean that is passed
down to ToggleableTimerForm.
EditableTimer
This uses the prop editFormOpen and also accepts all the attributes of a timer.
Timer
This uses all the attributes for a timer.
TimerForm
This has two interactive input fields, one for title and one for project. When editing an existing
timer, these fields are initialized with the timer’s current values.
State criteria
We can apply criteria to determine if data should be stateful:
These questions are from the excellent article by Facebook called “Thinking In React. You
can read the original article here
50
.
1. Is it passed in from a parent via props? If so, it probably isn’t state.
A lot of the data used in our child components are already listed in their parents. This criterion helps
us de-duplicate.
For example, “timer properties” is listed multiple times. When we see the properties declared in
EditableTimer, we can consider it state. But when we see it elsewhere, it’s not.
2. Does it change over time? If not, it probably isn’t state.
This is a key criterion of stateful data: it changes.
3. Can you compute it based on any other state or props in your component? If so, it’s not
state.
For simplicity, we want to strive to represent state with as few data points as possible.
Applying the criteria
App
isOpen boolean for ToggleableTimerForm and timer properties for EditableTimer
50
https://facebook.github.io/react/docs/thinking-in-react.html
React Fundamentals 93
Stateful. The data is defined here. It changes over time. And it cannot be computed from other state
or props.
timer attributes
Stateful. We define the data on each EditableTimer here. This data is mutable. And it cannot be
computed from other state or props.
EditableTimer
editFormOpen for a given timer
Stateful. The data is defined here. It changes over time. And it cannot be computed from other state
or props.
Timer
Timer properties
In this context, not stateful. Properties are passed down from the parent.
TimerForm
We might be tempted to conclude that TimerForm doesn’t manage any stateful data, as title and
project are props passed down from the parent. However, as saw with our SearchInput component
in the previous chapter, components that use TextInput can be special state managers in their own
right these components often maintain the value of the input field as state.
So, outside of TimerForm, we’ve identified our stateful data:
The list of timers and properties of each timer
Whether or not the edit form of a timer is open
Whether or not the create form is open
Step 4: Determine in which component each piece of
state should live
While the data we’ve determined to be stateful might live in certain components in our static app,
this does not indicate the best position for it in our stateful app. Our next task is to determine the
optimal place for each of our three discrete pieces of state to live.
This can be challenging at times but, again, we can apply the following steps from Facebook’s guide
Thinking in React
51
to help us with the process:
51
https://facebook.github.io/react/docs/thinking-in-react.html
React Fundamentals 94
For each piece of state:
Identify every component that renders something based on that state.
Find a common owner component (a single component above all the components
that need the state in the hierarchy).
Either the common owner or another component higher up in the hierarchy should
own the state.
If you can’t find a component where it makes sense to own the state, create a new
component simply for holding the state and add it somewhere in the hierarchy above
the common owner component.
Let’s apply this method to our application:
The list of timers and attributes of each timer
At first glance, we may be tempted to conclude that App does not appear to use this state. Instead, the
first component that uses this state is EditableTimer. We might think it would be wise to move timer
attributes into the EditableTimer component’s state as opposed to passing them down as props.
While this may be the case for displaying timers, modifying them, and deleting them, what about
creating them? ToggleableTimerForm does not need the state to render, but it can affect state. It
needs to be able to insert a new timer. It will propagate the data for the new timer up to the root
App.
Therefore, App is truly the common owner. It renders EditableTimer components by passing down
timer state. It can handle modifications from EditableTimer and creates from ToggleableTimerForm,
mutating the state. The new state will then flow downward through EditableTimer via props.
Whether or not the edit form of a timer is open
In our static app, App specifies whether or not an EditableTimer should be rendered with its edit
form open. Technically, though, this state could just live in each individual EditableTimer. No parent
component in the hierarchy depends on this data.
Storing the state in EditableTimer will be fine for our current needs. But there are a few requirements
that might require us to “hoist” this state up higher in the component hierarchy in the future.
For instance, what if we wanted to impose a restriction such that only one form, including the create
form, could be open at a time? Then it would make sense for App to own the state, as it would need
to inspect it to determine whether to allow for another form to open.
Visibility of the create form
App doesn’t appear to care about whether ToggleableTimerForm is open or closed. It feels safe to
reason that the state can just live inside ToggleableTimerForm itself.
So, in summary, we’ll have three pieces of state each in three different components:
React Fundamentals 95
Timer data will be owned and managed by App.
Each EditableTimer will manage the state of its timer edit form.
The ToggleableTimerForm will manage the state of its form visibility.
Step 5: Hardcode initial states
We’re now well prepared to make our app stateful. We’ll define our initial states within the
components themselves. This means hardcoding a list of timers in the top-level component, App.
For our two other pieces of state, we’ll have the components’ forms closed by default.
After we’ve added initial state to a parent component, we’ll make sure our props are properly
established in its children.
Adding state to App
Let’s start by modifying App to hold the timer data in state.
We’ll be using the npm library uuid
52
to generate ids for each of our timers. The library’s function
uuidv4() will randomly generate a Universally Unique IDentifier
53
for each of our timers.
A UUID is a string that looks like this:
2030efbd-a32f-4fcc-8637-7c410896b3e3
First, in your console, install the library:
yarn add uuid
Then, at the top of App.js, import the uuidv4() function:
time-tracking/2/App.js
import uuidv4 from 'uuid/v4';
Next, we’ll initialize the component’s state to an array of two timer objects. This will give us a list
of timers to play with when we open the app:
52
https://www.npmjs.com/package/uuid
53
https://en.wikipedia.org/wiki/Universally_unique_identifier
React Fundamentals 96
time-tracking/2/App.js
export default class App extends React.Component {
state = {
timers: [
{
title: 'Mow the lawn',
project: 'House Chores',
id: uuidv4(),
elapsed: 5456099,
isRunning: true,
},
{
title: 'Bake squash',
project: 'Kitchen Chores',
id: uuidv4(),
elapsed: 1273998,
isRunning: false,
},
],
};
We set the initial state to an object with the key timers. timers points to an array with two hardcoded
timer objects.
As in the previous chapter, we’re leaning on the Babel plugin transform-class-properties
to help simplify how we define our initial state.
Below, in render, we’ll use state.timers to generate an array of EditableTimer components. Each
will be derived from an individual object in the timers array that’s being passed in as a prop. We’ll
use map to do so:
time-tracking/2/App.js
render() {
const { timers } = this.state;
return (
<View style={styles.appContainer}>
<View style={styles.titleContainer}>
<Text style={styles.title}>Timers</Text>
</View>
<ScrollView style={styles.timerList}>
React Fundamentals 97
<ToggleableTimerForm />
{timers.map(({ title, project, id, elapsed, isRunning }) => (
<EditableTimer
key={id}
id={id}
title={title}
project={project}
elapsed={elapsed}
isRunning={isRunning}
/>
))}
</ScrollView>
</View>
);
}
The rendered UI of the component ends up being an array of EditableTimer components:
[
<EditableTimer
timer={{
title: 'Mow the lawn',
project: 'House Chores',
id: // random UUID,
elapsed: 5456099,
isRunning: true,
}}
/>,
<EditableTimer
timer={{
title: 'Bake squash',
project: 'Kitchen Chores',
id: // random UUID,
elapsed: 1273998,
isRunning: false,
}}
/>
]
Notably, we’re able to represent the EditableTimer component instance in JSX inside of return. It
might seem odd at first that we’re able to have a JavaScript array of JSX elements, but remember
that Babel will transpile the JSX representation of each EditableTimer (<EditableTimer />) into
regular JavaScript.
React Fundamentals 98
If you’re interested in how this compiles, please refer to the Appendix.
Note the use of the key={timer.id} prop. The key prop is not used by our EditableTimer
component but by the React Native framework. It’s a special property that we discuss deeper
in the next chapter “Core Components. For the time being, it’s enough to note that this
property needs to be unique per React Native component in a list.
Array’s map()
If you’re unfamiliar with the map method, it takes a function as an argument and calls it with
each item inside of the array and builds a new array by using the return value from each
function call.
Since the timers array has two items, map will call this function twice, once for each timer.
When map calls this function, it passes in as the first argument an item. The return value
from this function call is inserted into the new array that map is constructing. After handling
the last item, map returns this new array. Here, we’re rendering this new array within our
render() method.
Props vs. state
Let’s take a step back and reflect on the difference between props and state again. What existed as
mutable state in App is passed down as immutable props to EditableTimer.
We talked at length about what qualifies as state and where state should live. Mercifully, we do not
need to have an equally lengthy discussion about props. Once you understand state, you can see
how props act as its one-way data pipeline. State is managed in some select parent components
and then that data flows down through children as props.
If state is updated, the component managing that state re-renders by calling render(). This, in turn,
causes any of its children to re-render as well. And the children of those children. And on and on
down the chain.
Let’s continue our own march down the chain.
Adding state to EditableTimer
In the static version of our app, EditableTimer relied on editFormOpen as a prop to be passed down
from the parent. We decided that this state could actually live here in the component itself.
Because this component will actually manage state, we’ll need to change it from a functional
component to a class component.
We’ll set the initial value of editFormOpen to false, which means that the form starts off as closed:
React Fundamentals 99
time-tracking/2/components/EditableTimer.js
export default class EditableTimer extends React.Component {
state = {
editFormOpen: false,
};
render() {
const { id, title, project, elapsed, isRunning } = this.props;
const { editFormOpen } = this.state;
if (editFormOpen) {
return <TimerForm id={id} title={title} project={project} />;
}
return (
<Timer
id={id}
title={title}
project={project}
elapsed={elapsed}
isRunning={isRunning}
/>
);
}
}
Timer remains stateless
If you look at Timer, you’ll see that it does not need to be modified to include state. It has been using
exclusively props and is so far unaffected by our current refactor.
Adding state to ToggleableTimerForm
We know that we’ll need to tweak ToggleableTimerForm as we’ve assigned it some stateful
responsibility. We want to have the component manage the state isOpen.
We’ll initialize the state to a “closed” state at the top of the component:
React Fundamentals 100
time-tracking/2/components/ToggleableTimerForm.js
export default class ToggleableTimerForm extends React.Component {
state = {
isOpen: false,
};
Next, we’ll define a function that toggles the state of the form to open:
time-tracking/2/components/ToggleableTimerForm.js
handleFormOpen = () => {
this.setState({ isOpen: true });
};
Finally, we’ll modify the component’s render() method to include our app’s first piece of interactiv-
ity. We’ll switch off of isOpen to determine whether we should render the “+” button or a TimerForm.
We’ll set handleFormOpen as the onPress handler for TimerButton:
time-tracking/2/components/ToggleableTimerForm.js
render() {
const { isOpen } = this.state;
return (
<View style={[styles.container, !isOpen && styles.buttonPadding]}>
{isOpen ? (
<TimerForm />
) : (
<TimerButton title="+" color="black" onPress={this.handleFormOpen} />
)}
</View>
);
}
If you remember, we created TimerButton to accept an onPress prop which is passed down to
the onPress action of the TouchableOpacity within. TouchableOpacity is a built-in React Native
component. When it is pressed, it will invoke its onPress handler. TimerButton passes along its own
onPress prop directly to TouchableOpacity.
Therefore, when the TimerButton is pressed, the function handleFormOpen() will be invoked.
handleFormOpen() modifies the state, setting isOpen to true. This causes the ToggleableTimerForm
component to re-render. When render() is called this second time around, this.state.isOpen is
true and ToggleableTimerForm renders TimerForm. Neat.
React Fundamentals 101
As we explored in the last chapter, we are writing the handleFormOpen() function as a
property initializer (i.e. using an arrow function) in order to ensure this inside the function
is bound to the component. React will automatically bind class methods corresponding to
the component API (like render and componentDidMount) to the component for us.
Our updated ToggleableTimerForm, in full:
time-tracking/2/components/ToggleableTimerForm.js
export default class ToggleableTimerForm extends React.Component {
state = {
isOpen: false,
};
handleFormOpen = () => {
this.setState({ isOpen: true });
};
render() {
const { isOpen } = this.state;
return (
<View style={[styles.container, !isOpen && styles.buttonPadding]}>
{isOpen ? (
<TimerForm />
) : (
<TimerButton title="+" color="black" onPress={this.handleFormOpen} />
)}
</View>
);
}
}
Adding state to TimerForm
We mentioned earlier that TimerForm would manage state as it includes a form. In React Native,
forms are stateful.
Recall that TimerForm includes two input fields:
React Fundamentals 102
These input fields are modifiable by the user. In React Native, all modifications that are made to a
component should be handled and kept in state. This includes changes like the modification of an
input field. The best way to understand this is to see what it looks like.
To make these input fields stateful, we can make our component stateful and initialize state at the
top of our component:
time-tracking/2/components/TimerForm.js
export default class TimerForm extends React.Component {
constructor(props) {
super(props);
const { id, title, project } = props;
this.state = {
title: id ? title : '',
project: id ? project : '',
};
}
Our state object has two properties, each corresponding to an input field that TimerForm manages.
If TimerForm is creating a new timer as opposed to editing an existing one, the id prop will be
undefined. In that case, we initialize both properties to a blank string ('') using ternary operators.
Otherwise, when this form is editing a timer, we’ll want to set both values to their respective prop
values.
Note that because we’re checking and defining our state based on props, we’re using the constructor()
for state initialization instead of defining state as a class property.
We want to avoid initializing title or project to undefined. That’s because the value of
an input field can’t technically ever be undefined. If it’s empty, its value in JavaScript is a
blank string.
In our first pass at building this component, we used defaultValue to set the initial state of the
TextInput fields based on props. But the defaultValue prop only sets the value of the TextInput
React Fundamentals 103
for the initial render. Instead of using defaultValue, we can connect our input fields directly to our
component’s state using value. We could do something like this:
<TextInput value={this.state.title} />
With this, our input fields would be driven by state. Whenever either of our state properties change,
our input fields would be updated to reflect the new value.
However, this misses a key ingredient: We don’t currently have any way for the user to modify
this state. The input field will start off in-sync with the component’s state. But the moment the user
makes a modification, the input field will become out-of-sync with the component’s state.
We can fix this by using React Native’s onChangeText prop for TextInput components. Like onPress
for a button component, we can set onChangeText to a function like we did in our previous chapter.
Whenever the input field is changed, React will invoke the function specified.
Let’s set the onChangeText attributes on both input fields to functions we’ll define next. For our title
input field:
time-tracking/2/components/TimerForm.js
<TextInput
style={styles.textInput}
underlineColorAndroid="transparent"
onChangeText={this.handleTitleChange}
value={title}
/>
And similarly for project:
time-tracking/2/components/TimerForm.js
<TextInput
style={styles.textInput}
underlineColorAndroid="transparent"
onChangeText={this.handleProjectChange}
value={project}
/>
The functions handleTitleChange and handleProjectChange will both modify their respective
properties in state. Here’s what they look like:
React Fundamentals 104
time-tracking/2/components/TimerForm.js
handleTitleChange = title => {
this.setState({ title });
};
handleProjectChange = project => {
this.setState({ project });
};
When React Native invokes the function passed to onChangeText, it invokes the function with the
changed text passed as the argument. With this, we update the state to the new value of the input
field.
Using a combination of state, the value attribute, and the onChangeText attribute is the
canonical method we use to write form elements in React Native.
Our updated TimerForm component, in full:
time-tracking/2/components/TimerForm.js
export default class TimerForm extends React.Component {
constructor(props) {
super(props);
const { id, title, project } = props;
this.state = {
title: id ? title : '',
project: id ? project : '',
};
}
handleTitleChange = title => {
this.setState({ title });
};
handleProjectChange = project => {
this.setState({ project });
};
render() {
const { id } = this.props;
const { title, project } = this.state;
React Fundamentals 105
const submitText = id ? 'Update' : 'Create';
return (
<View style={styles.formContainer}>
<View style={styles.attributeContainer}>
<Text style={styles.textInputTitle}>Title</Text>
<View style={styles.textInputContainer}>
<TextInput
style={styles.textInput}
underlineColorAndroid="transparent"
onChangeText={this.handleTitleChange}
value={title}
/>
</View>
</View>
<View style={styles.attributeContainer}>
<Text style={styles.textInputTitle}>Project</Text>
<View style={styles.textInputContainer}>
<TextInput
style={styles.textInput}
underlineColorAndroid="transparent"
onChangeText={this.handleProjectChange}
value={project}
/>
</View>
</View>
<View style={styles.buttonGroup}>
<TimerButton small color="#21BA45" title={submitText} />
<TimerButton small color="#DB2828" title="Cancel" />
</View>
</View>
);
}
}
To recap, here’s an example of the lifecycle of TimerForm:
1. On the page is a timer with the title “Mow the lawn.
2. The user toggles open the edit form for this timer, mounting TimerForm to the screen.
3. TimerForm initializes the state property title to the string "Mow the lawn".
4. The user modifies the title input field, changing it to the value "Cut the grass".
5. With every keystroke, React invokes handleTitleChange. The internal state of title is kept
in-sync with what the user sees on the page.
React Fundamentals 106
With TimerForm refactored, we’ve finished establishing our stateful data inside our components.
And we’ve assembled our downward data pipeline, props.
We’re ready and perhaps a bit eager to build out interactivity using inverse data flow. But
before we do, let’s save and reload the app to ensure everything is working.
Try it out
We expect to see new example timers based on the hardcoded data in App. We also expect pressing
the “+” button toggles open a form:
Step 6: Add inverse data flow
As we saw in the last chapter, children communicate with parents by calling functions that are
provided to them via props. In our weather app, when our search field was submitted with a value,
SearchInput didn’t do any data management. Instead, it called a function given to it by App, which
was then able to manage state accordingly.
We are going to need inverse data flow in two areas:
TimerForm needs to propagate create and update events (create while under ToggleableTimerForm
and update while under EditableTimer). Both events will eventually reach our top level App.
Timer has a fair amount of behavior. It needs to handle delete and edit press, as well as the
start and stop timer logic.
Let’s start with TimerForm.
React Fundamentals 107
TimerForm
To get a clear idea of what exactly TimerForm will require, we’ll start by adding event handlers to it
and then we’ll work our way backwards up the hierarchy.
TimerForm needs two event handlers:
When the form is submitted (creating or updating a timer)
When the “Cancel” button is pressed (closing the form)
TimerForm
will receive two functions as props to handle each event. The parent component that uses
TimerForm is responsible for providing these functions:
props.onFormSubmit(): called when the form is submitted
props.onFormClose()
: called when the “Cancel” button is pressed
As we’ll see soon, this enables the parent component to determine what the behavior should be
when these events occur.
Let’s first add onFormClose to the props being destructured in the component’s render method:
time-tracking/3/components/TimerForm.js
render() {
const { id, onFormClose } = this.props;
We’ll then modify the buttons on TimerForm by specifying onPress props for each:
time-tracking/3/components/TimerForm.js
<View style={styles.buttonGroup}>
<TimerButton
small
color="#21BA45"
title={submitText}
onPress={this.handleSubmit}
/>
<TimerButton
small
color="#DB2828"
title="Cancel"
onPress={onFormClose}
/>
</View>
React Fundamentals 108
The onPress prop for the “Submit” button specifies the function this.handleSubmit, which we’ll
define next. The onPress prop for the “Cancel” button specifies the prop onFormClose directly.
Now that we’ve seen how we’ll use handleSubmit, let’s write it. Declare this function above
render():
time-tracking/3/components/TimerForm.js
handleSubmit = () => {
const { onFormSubmit, id } = this.props;
const { title, project } = this.state;
onFormSubmit({
id,
title,
project,
});
};
Again, we’re working bottom-up right now. So the handleSubmit() method calls the anticipated
function onFormSubmit() which we’ll write in a moment. It passes in a data object with id, title,
and project attributes.
Notice that we’re reading id via props and reading title and project from state. This is because we
want to supply the function with the up-to-date values of title and project (in state) as opposed
to the initial values (supplied as props).
ToggleableTimerForm
Let’s follow the submit event from TimerForm as it bubbles up the component hierarchy. First, we’ll
modify ToggleableTimerForm. We need it to define and pass down two prop-functions to TimerForm:
onFormClose() and onFormSubmit().
Let’s update the component’s render() method first:
time-tracking/4/components/ToggleableTimerForm.js
render() {
const { isOpen } = this.state;
return (
<View style={[styles.container, !isOpen && styles.buttonPadding]}>
{isOpen ? (
<TimerForm
onFormSubmit={this.handleFormSubmit}
onFormClose={this.handleFormClose}
React Fundamentals 109
/>
) : (
<TimerButton title="+" color="black" onPress={this.handleFormOpen} />
)}
</View>
);
}
We pass in two functions as props to TimerForm. As we’ve seen, functions are just like any other
prop.
Let’s write handleFormClose() first:
time-tracking/4/components/ToggleableTimerForm.js
handleFormClose = () => {
this.setState({ isOpen: false });
};
Now, what might handleFormSubmit() look like? ToggleableTimerForm is not the manager of timer
state. So ToggleableTimerForm should expect a new prop, onFormSubmit() from App. ToggleableTimerForm
should, in turn, pass this down to TimerForm. When the user submits a form down in TimerForm,
they’ll be invoking a function defined up in App that modifies the timer state. ToggleableTimerForm
is just a proxy of this function.
So, we might be tempted to just pass this anticipated prop-function directly to TimerForm like this:
<TimerForm
onFormSubmit={this.props.onFormSubmit}
onFormClose={this.handleFormClose}
/>
However, consider this: after the user clicks “Create to create a timer, we actually want to close
ToggleableTimerForm. We’ll want to intercept this event so that we can set isOpen to false.
To do this, here’s what handleFormSubmit() looks like:
React Fundamentals 110
time-tracking/4/components/ToggleableTimerForm.js
handleFormSubmit = timer => {
const { onFormSubmit } = this.props;
onFormSubmit(timer);
this.setState({ isOpen: false });
};
The handleFormSubmit() method accepts the argument timer and passes it along to onFormSubmit().
Recall that in TimerForm this argument is an object containing the desired timer properties. After
invoking onFormSubmit(), handleFormSubmit() calls setState() to close its form.
Although we’re not adding server communication in this chapter, let’s try and visualize how
submitting the form would work if we did.
The result of onFormSubmit() will not impact whether or not the form is closed. We invoke
onFormSubmit(), which may eventually create an asynchronous call to a server. Execution
will continue before we hear back from the server which means setState() will be called.
If onFormSubmit() fails such as if the server is temporarily unreachable we’d ideally
have some way to display an error message and re-open the form.
App
We’ve reached the top of the hierarchy, our root App component. As this component will be
responsible for the data for the timers, it is here that we will define the logic for handling the events
we’re capturing down at the lowest-level components.
The first event we’re concerned with is the submission of a form (i.e. events from TimerForm). When
this happens, either a new timer is being created or an existing one is being updated. We’ll create
two separate functions to handle these two distinct events:
handleCreateFormSubmit() will handle creating timers and will be the function passed to
ToggleableTimerForm
handleFormSubmit() will handle updating timers and will be the function passed to EditableTimer
Both functions travel down their respective component hierarchies until they reach TimerForm as
the prop onFormSubmit().
Let’s start with handleCreateFormSubmit().
Handling creates
handleCreateFormSubmit() will be the function App will supply to ToggleableTimerForm. Let’s set
that prop now:
React Fundamentals 111
time-tracking/3/App.js
<ToggleableTimerForm onFormSubmit={this.handleCreateFormSubmit} />
Next, we’ll define the function.
For creating timers, we’ll be using the function newTimer() from utils/TimerUtils.js. This just
hides some logic, like generating ids. Here’s what it looks like:
time-tracking/utils/TimerUtils.js
export const newTimer = (attrs = {}) => {
const timer = {
title: attrs.title || 'Timer',
project: attrs.project || 'Project',
id: uuidv4(),
elapsed: 0,
isRunning: false,
};
return timer;
};
The function accepts an object with timer and project properties and returns a new object with the
rest of the properties properly initialized.
Import that function at the top of App.js:
time-tracking/3/App.js
import { newTimer } from './utils/TimerUtils';
Inside handleCreateFormSubmit(), we’ll use newTimer() to insert a new timer object into state.
Declare this function above render():
time-tracking/3/App.js
handleCreateFormSubmit = timer => {
const { timers } = this.state;
this.setState({
timers: [newTimer(timer), ...timers],
});
};
React Fundamentals 112
Note that we set this.state.timers to a new array of timers. The first element in the array is our
new timer, created with newTimer(). Then, we use JavaScript’s spread syntax to add the rest of our
existing timers to this new array. We do this to avoid mutating state.
The tempting alternative is to write handleCreateFormSubmit() like this:
handleCreateFormSubmit = timer => {
const { timers } = this.state;
this.setState({
timers: timers.push(newTimer(timer)), // mutates state!
});
};
But .push() appends the new timer to the existing array in state. It’s subtle, but this mutates
state. And we never want to mutate state outside of the this.setState() method.
We always want to treat the state object (and the objects and arrays inside state) as
immutable. Writing immutable JavaScript can be tricky at first. A simple strategy is to just
avoid using certain Array and Object methods. .push() is one method to avoid, as it always
mutates the array it is called on.
If you still find the distinction between .push() and the spread syntax confusing, don’t
worry. We’ll be showcasing strategies for how to avoid accidental state mutations through-
out the book.
Spread syntax
In arrays, the ellipsis (…) will expand the array that follows into the parent array. The spread
operator enables us to succinctly construct new arrays as a composite of existing arrays:
const a = [ 1, 2, 3 ];
const b = [ 4, 5, 6 ];
const c = [ ...a, ...b, 7, 8, 9 ];
console.log(c); // -> [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
In objects, the ellipsis (…) will allow you to create a modified version of an existing object:
const coffee = { milk: false, cream: false };
const coffeeWithMilk = { ...coffee, milk: true };
console.log(coffeeWithMilk); // -> { milk: true, cream: false }
This can be useful when working with immutable JavaScript objects.
React Fundamentals 113
Try it out
Now we’ve finished wiring up the create timer flow from the form down in TimerForm up to the
state managed in App. Save App.js and your app should reload. Toggle open the create form and
create some new timers:
Updating timers
Our app is setup for creating timers. Let’s add updates next.
We know we’ll eventually want App to define a handler, onFormSubmit(), for when TimerForm
submits a timer update. However, as you can see in the current state of the app, we haven’t yet
added the ability for a timer to be edited. We don’t yet have a way to display an edit form which
will be a prerequisite to submitting one.
To display an edit form, the user will press on the edit button on a Timer. This should propagate an
event up to EditableTimer and tell it to flip its child component, opening the form.
We’ll work from the bottom-up again. We’ll start with Timer, specify the prop-functions that it needs,
then move up the component hierarchy.
Adding editability to Timer
Since we’ll be adding a fair bit of functionality to Timer, we’ll first convert it into a class component:
React Fundamentals 114
time-tracking/4/components/Timer.js
export default class Timer extends React.Component {
EditableTimer manages the state of whether or not the edit form is open. So, we’ll expect Timer to
receive a prop from its parent, onEditPress(). We’ll set the onPress prop on the “Edit” TimerButton
to this prop:
time-tracking/4/components/Timer.js
render() {
const { elapsed, title, project, onEditPress } = this.props;
const elapsedString = millisecondsToHuman(elapsed);
return (
<View style={styles.timerContainer}>
<Text style={styles.title}>{title}</Text>
<Text>{project}</Text>
<Text style={styles.elapsedTime}>{elapsedString}</Text>
<View style={styles.buttonGroup}>
<TimerButton color="blue" small title="Edit" onPress={onEditPress} />
<TimerButton color="blue" small title="Remove" />
</View>
<TimerButton color="#21BA45" title="Start" />
</View>
);
}
Updating EditableTimer
Now we’re prepared to update EditableTimer. Again, it will display either the TimerForm (if we’re
editing) or an individual Timer (if we’re not editing).
Let’s add event handlers for both possible child components. For TimerForm, we want to handle the
form being closed or submitted. For Timer, we want to handle the edit button being pressed:
React Fundamentals 115
time-tracking/4/components/EditableTimer.js
export default class EditableTimer extends React.Component {
state = {
editFormOpen: false,
};
handleEditPress = () => {
this.openForm();
};
handleFormClose = () => {
this.closeForm();
};
handleSubmit = timer => {
const { onFormSubmit } = this.props;
onFormSubmit(timer);
this.closeForm();
};
closeForm = () => {
this.setState({ editFormOpen: false });
};
openForm = () => {
this.setState({ editFormOpen: true });
};
We pass these event handlers down as props:
time-tracking/4/components/EditableTimer.js
render() {
const { id, title, project, elapsed, isRunning } = this.props;
const { editFormOpen } = this.state;
if (editFormOpen) {
return (
<TimerForm
id={id}
title={title}
project={project}
React Fundamentals 116
onFormSubmit={this.handleSubmit}
onFormClose={this.handleFormClose}
/>
);
}
return (
<Timer
id={id}
title={title}
project={project}
elapsed={elapsed}
isRunning={isRunning}
onEditPress={this.handleEditPress}
/>
);
}
Look a bit familiar? EditableTimer handles the same events emitted from TimerForm in a similar
manner as ToggleableTimerForm. This makes sense. Both EditableTimer and ToggleableTimerForm
are just intermediaries between TimerForm and App. App is the one that defines the submit function
handlers.
Like ToggleableTimerForm, EditableTimer doesn’t do anything with the incoming timer. In
handleSubmit(), it just blindly passes this object along to its prop-function onFormSubmit(). It then
closes the form with closeForm().
We pass along our new prop to Timer, onEditPress. The behavior for this function is defined in
handleEditPress, which modifies the state for EditableTimer, opening the form.
Defining handleFormSubmit() in App
Like we did with handleCreateFormSubmit(), the last step with this pipeline is to define a handler
for edit form submits up in App, handleFormSubmit().
For creating timers, we have a function that creates a new timer object with the specified attributes
and we prepend this new object to the beginning of the timers array in state.
For updating timers, we need to hunt through the timers array until we find the timer object that is
being updated. As always, the state object cannot be updated directly. We have to use setState().
Therefore, we’ll use map() to traverse the array of timer objects. If the timer’s id matches that of
the form submitted, we’ll return a new object that contains the timer with the updated attributes.
Otherwise we’ll just return the original timer. This new array of timer objects will be passed to
setState():
React Fundamentals 117
time-tracking/4/App.js
handleFormSubmit = attrs => {
const { timers } = this.state;
this.setState({
timers: timers.map(timer => {
if (timer.id === attrs.id) {
const { title, project } = attrs;
return {
...timer,
title,
project,
};
}
return timer;
}),
});
};
Note that we call map() on timers from within the JavaScript object we’re passing to setState().
This is an often used pattern. The call is evaluated and then the property timers is set to the result.
Inside of the map() function we check if the timer matches the one being updated by comparing
their id attributes. If not, we just return the timer. Otherwise, we use the spread operator again to
return a new object with the timer’s updated attributes.
Remember, it’s important here that we treat state as immutable. By creating a new timers object and
then using the spread operator to populate it, we’re not modifying any of the objects sitting in state.
We pass this method down as a prop inside render() to EditableTimer:
time-tracking/4/App.js
{timers.map(({ title, project, id, elapsed, isRunning }) => (
<EditableTimer
key={id}
id={id}
title={title}
project={project}
elapsed={elapsed}
isRunning={isRunning}
onFormSubmit={this.handleFormSubmit}
/>
))}
React Fundamentals 118
As we did with ToggleableTimerForm and handleCreateFormSubmit, we pass down handleFormSubmit
as the prop onFormSubmit. TimerForm calls this prop, oblivious to the fact that this function is entirely
different when it is rendered underneath EditableTimer as opposed to ToggleableTimerForm.
Try it out
Both of the forms are wired up! Save App.js, and after your app reloads, try both creating and
updating timers. You can also press “Cancel” on an open form to close it:
Note that the keyboard might get in the way when you’re typing into an edit form. We’ll address
this at the end of the chapter by using the KeyboardAvoidingView component in App.
The rest of our work resides within the timer. We need to:
Wire up the “Remove” button
Implement the start/stop buttons and the timing logic itself
Try it yourself
Feeling ambitious? Before moving on to the next section, see how far you can get wiring up
the “Remove” button by yourself. Move ahead afterwards and verify your solution is sound.
React Fundamentals 119
Deleting timers
Adding the event handler to Timer
As with adding create and update functionality, we’ll work from the bottom-up. We’ll start in Timer
and work our way up to App. In App is where we’ll define the function that removes the targeted
timer from state.
In Timer, we’ll begin by defining the function for handling “Remove” button press events:
time-tracking/5/components/Timer.js
handleRemovePress = () => {
const { id, onRemovePress } = this.props;
onRemovePress(id);
};
We’ve yet to define the function that will be set as the prop onRemovePress(). But you can imagine
that when this event reaches the top (App), we’re going to need the id to sort out which timer is
being deleted. The handleRemovePress() method provides the id to this function.
We use onPress to connect that function to the “Remove TimerButton:
time-tracking/5/components/Timer.js
<TimerButton
color="blue"
small
title="Remove"
onPress={this.handleRemovePress}
/>
Routing through EditableTimer
In EditableTimer, we include onRemovePress in the destructured props in the component’s render
method:
React Fundamentals 120
time-tracking/5/components/EditableTimer.js
render() {
const {
id,
title,
project,
elapsed,
isRunning,
onRemovePress,
} = this.props;
We then pass the function along to Timer:
time-tracking/5/components/EditableTimer.js
<Timer
id={id}
title={title}
project={project}
elapsed={elapsed}
isRunning={isRunning}
onEditPress={this.handleEditPress}
onRemovePress={onRemovePress}
/>
Implementing the remove function in App
The last step is to define the function in App that removes the desired timer from the state array.
There are many ways to accomplish this in JavaScript. If you attempted to implement this solution
on your own, don’t sweat it if your solution was not the same.
We add our handler function that we will ultimately pass down as a prop:
time-tracking/5/App.js
handleRemovePress = timerId => {
this.setState({
timers: this.state.timers.filter(t => t.id !== timerId),
});
};
Here, we use the Array filter() method to return a new array without the timer object that has an
id matching timerId.
Finally, we pass down handleRemovePress() as a prop:
React Fundamentals 121
time-tracking/5/App.js
<EditableTimer
key={id}
id={id}
title={title}
project={project}
elapsed={elapsed}
isRunning={isRunning}
onFormSubmit={this.handleFormSubmit}
onRemovePress={this.handleRemovePress}
/>
Array filter()
Array filter() accepts a function that is used to “test” each element in the array. It returns
a new array containing all the elements that “passed” the test. If the function returns true,
the element is kept.
Try it out
Save App.js and reload the app. Now you can delete timers:
React Fundamentals 122
Adding timing functionality
Functionality for creating, updating, and deleting is now in place for our timers. The next challenge:
making these timers actually track time.
There are several different ways we can implement a timer system. The simplest approach would
be to have a function update the elapsed property on each timer every second. This is why we’ve
included the timer property isRunning. We can do something like this:
this.setState({
timers: timers.map(timer => {
const { elapsed, isRunning } = timer;
return {
...timer,
elapsed: isRunning ? elapsed + 1000 : elapsed,
};
}),
});
We map through all the timers in state and check the value of isRunning. If isRunning is true, we
can add 1000 milliseconds (or 1 second) to elapsed.
Now, to make this work we’ll need to do this every second. We can use JavaScript’s setInterval()
to execute this function on an interval.
Let’s set up our interval in componentDidMount:
time-tracking/6/App.js
componentDidMount() {
const TIME_INTERVAL = 1000;
this.intervalId = setInterval(() => {
const { timers } = this.state;
this.setState({
timers: timers.map(timer => {
const { elapsed, isRunning } = timer;
return {
...timer,
elapsed: isRunning ? elapsed + TIME_INTERVAL : elapsed,
};
}),
React Fundamentals 123
});
}, TIME_INTERVAL);
}
setInterval() accepts two arguments. The first argument is the function we’d like executed on an
interval. Here, we’re performing the logic to update elapsed. The second argument is the length
of the interval (or the delay between function invocations). We set that to TIME_INTERVAL, 1000
milliseconds.
We also capture the return value of setInterval(), setting the component variable this.intervalId.
This special identifier allows us to stop the interval at any point in the future using JavaScript’s cor-
responding clearInterval(). We’ll want to cancel (or “clear”) this interval if the timer component
is ever unmounted (deleted). Otherwise, our function will run on indefinitely and cause errors. We
can use the componentWillUnmount lifecycle hook for this:
time-tracking/6/App.js
componentWillUnmount() {
clearInterval(this.intervalId);
}
This is the first time we’re using componentWillUnmount. Like the name suggests, this method fires
right before a component is unmounted or removed. In this example, we use clearInterval() to
cancel the logic that updates our timers.
Using setInterval() when a component mounts and clearInterval() when a component un-
mounts is a common pattern in React apps that require interval events.
In our version of the app, App will never be unmounted. However, it is still best practice
to clear any intervals when a component unmounts. This “future proofs” our app. There
are many libraries, like “hot reloading” libraries, that would cause even the app’s main
component to be unmounted and re-mounted.
Although this timer implementation works for our purposes, it is not the most accurate.
There is no guarantee that timers will be updated precisely every 1000 milliseconds and we
lose accuracy around starts and stops.
An example of a more precise approach would be defining a separate timer attribute, like
runningSince. We could then derive how long a timer has been running by calculating the
difference between the value of runningSince and the current time. If we saved this value
somewhere, it would also allow our timers to continue “running” even while the app is
closed.
React Fundamentals 124
Add start and stop functionality
With our interval in place, we just need to add the ability to flip the isRunning boolean on a given
timer.
The action button at the bottom of each timer should display “Start” if the timer is paused and
“Stop if the timer is running. These presses will invoke functions defined in App that will modify
isRunning.
Add timer action events to
Timer
We’ll start at the bottom again with Timer.
We’ll anticipate two prop-functions, onStartPress() and onStopPress(). Let’s write the button
press event handlers that will call these functions first:
time-tracking/6/components/Timer.js
handleStartPress = () => {
const { id, onStartPress } = this.props;
onStartPress(id);
};
handleStopPress = () => {
const { id, onStopPress } = this.props;
onStopPress(id);
};
We’re propagating the id property up to App so it knows which timer to start or stop.
Inside render(), we’ll anticipate a renderActionButton() method that conditionally shows the
correct button based on whether the timer is running or has stopped:
React Fundamentals 125
time-tracking/6/components/Timer.js
render() {
const { elapsed, title, project, onEditPress } = this.props;
const elapsedString = millisecondsToHuman(elapsed);
return (
<View style={styles.timerContainer}>
<Text style={styles.title}>{title}</Text>
<Text>{project}</Text>
<Text style={styles.elapsedTime}>{elapsedString}</Text>
<View style={styles.buttonGroup}>
<TimerButton color="blue" small title="Edit" onPress={onEditPress} />
<TimerButton
color="blue"
small
title="Remove"
onPress={this.handleRemovePress}
/>
</View>
{this.renderActionButton()}
</View>
);
}
Now let’s set up the JSX rendered within this method:
time-tracking/6/components/Timer.js
renderActionButton() {
const { isRunning } = this.props;
if (isRunning) {
return (
<TimerButton
color="#DB2828"
title="Stop"
onPress={this.handleStopPress}
/>
);
}
return (
<TimerButton
React Fundamentals 126
color="#21BA45"
title="Start"
onPress={this.handleStartPress}
/>
);
}
We could write this conditional inside of the component’s render() method. But, as we briefly
mentioned in the previous chapter, a common pattern in React is to use helper methods to do this.
Sometimes this helps with code clarity and readability. In renderActionButton(), we specifically
render one button or another based on this.props.isRunning.
Now we’ll need to run these events up the component hierarchy, all the way up to App where we’re
managing state.
Run the events through EditableTimer
In EditableTimer, we’ll need to pass onStartPress and onStopPress to Timer:
time-tracking/6/components/EditableTimer.js
render() {
const {
id,
title,
project,
elapsed,
isRunning,
onRemovePress,
onStartPress,
onStopPress,
} = this.props;
const { editFormOpen } = this.state;
if (editFormOpen) {
return (
<TimerForm
id={id}
title={title}
project={project}
onFormSubmit={this.handleSubmit}
onFormClose={this.handleFormClose}
/>
);
React Fundamentals 127
}
return (
<Timer
id={id}
title={title}
project={project}
elapsed={elapsed}
isRunning={isRunning}
onEditPress={this.handleEditPress}
onRemovePress={onRemovePress}
onStartPress={onStartPress}
onStopPress={onStopPress}
/>
);
}
We can define a single function that handles these props in App. It should hunt through the state
timers array using map, flipping isRunning when it finds the matching timer:
time-tracking/6/App.js
toggleTimer = timerId => {
this.setState(prevState => {
const { timers } = prevState;
return {
timers: timers.map(timer => {
const { id, isRunning } = timer;
if (id === timerId) {
return {
...timer,
isRunning: !isRunning,
};
}
return timer;
}),
};
});
};
When toggleTimer comes across the relevant timer within its map call, it sets the property isRunning
to the opposite of its value. This means it will stop a running timer and start a stopped timer.
React Fundamentals 128
Finally, we pass this function down to EditableTimer in the render method:
time-tracking/6/App.js
<EditableTimer
key={id}
id={id}
title={title}
project={project}
elapsed={elapsed}
isRunning={isRunning}
onFormSubmit={this.handleFormSubmit}
onRemovePress={this.handleRemovePress}
onStartPress={this.toggleTimer}
onStopPress={this.toggleTimer}
/>
Try it out
Save App.js, wait for the app to reload, and behold: you can now create, update, and delete timers
as well as actually use them to time things!
Again, for this app we won’t add server communication. Without it, our app’s data is ephemeral. If
we reload the app, the timers will reset.
Wrapping App in KeyboardAvoidingView
A behavioral quirk in our app so far has been that the keyboard can get in the way when we edit a
timer. As we saw in the first chapter, we can wrap our app in a KeyboardAvoidingView component
React Fundamentals 129
to address this.
In App.js, first import the component:
time-tracking/6/App.js
import React from 'react';
import uuidv4 from 'uuid/v4';
import {
StyleSheet,
View,
ScrollView,
Text,
KeyboardAvoidingView,
Next, wrap the ScrollView component with it:
time-tracking/6/App.js
render() {
const { timers } = this.state;
return (
<View style={styles.appContainer}>
<View style={styles.titleContainer}>
<Text style={styles.title}>Timers</Text>
</View>
<KeyboardAvoidingView
behavior="padding"
style={styles.timerListContainer}
>
<ScrollView contentContainerStyle={styles.timerList}>
<ToggleableTimerForm onFormSubmit={this.handleCreateFormSubmit} />
{timers.map(({ title, project, id, elapsed, isRunning }) => (
<EditableTimer
key={id}
id={id}
title={title}
project={project}
elapsed={elapsed}
isRunning={isRunning}
onFormSubmit={this.handleFormSubmit}
onRemovePress={this.handleRemovePress}
onStartPress={this.toggleTimer}
React Fundamentals 130
onStopPress={this.toggleTimer}
/>
))}
</ScrollView>
</KeyboardAvoidingView>
</View>
);
}
Finally, add this style to the styles object:
time-tracking/6/App.js
timerListContainer: {
flex: 1,
},
Our ScrollView will now accommodate the keyboard when we start typing into text inputs.
To finish up, let’s add PropTypes to our components. As discussed in the previous chapter, PropTypes
are nice to have in place when making additions or changes to a React Native app.
PropTypes
Let’s add PropTypes to each of our components beginning with EditableTimer:
time-tracking/components/EditableTimer.js
export default class EditableTimer extends React.Component {
static propTypes = {
id
: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
project: PropTypes.string.isRequired,
elapsed: PropTypes.number.isRequired,
isRunning: PropTypes.bool.isRequired,
onFormSubmit: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired,
onStartPress: PropTypes.func.isRequired,
onStopPress: PropTypes.func.isRequired,
};
For this component, all our props are required as we’re expecting App to always set them. Now let’s
take a look at Timer:
React Fundamentals 131
time-tracking/components/Timer.js
export default class Timer extends Component {
static propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
project: PropTypes.string.isRequired,
elapsed: PropTypes.number.isRequired,
isRunning: PropTypes.bool.isRequired,
onEditPress: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired,
onStartPress: PropTypes.func.isRequired,
onStopPress: PropTypes.func.isRequired,
};
Similarly, we know each of the props for Timer should always be provided by EditableTimer. Let’s
add PropTypes and defaultProps for TimerButton:
time-tracking/components/TimerButton.js
TimerButton.propTypes = {
color: ColorPropType.isRequired,
title: PropTypes.string.isRequired,
small: PropTypes.bool,
onPress: PropTypes.func.isRequired,
};
TimerButton.defaultProps = {
small: false,
};
small is an optional prop with a default value of false. Our other props are all required. We’re also
using ColorPropType for our color prop in order to correctly validate if an appropriate color
54
is
passed in. We’ll need to import it at the top of our file from react-native as well.
Our last two components that need prop validations are our form components. Let’s begin with
TimerForm:
54
https://facebook.github.io/react-native/docs/next/colors.html
React Fundamentals 132
time-tracking/components/TimerForm.js
export default class TimerForm extends React.Component {
static propTypes = {
id: PropTypes.string,
title: PropTypes.string,
project: PropTypes.string,
onFormSubmit: PropTypes.func.isRequired,
onFormClose: PropTypes.func.isRequired,
};
static defaultProps = {
id: null,
title: '',
project: '',
};
For this component, we only pass in timer attributes (id, title, and project) if we’re editing
a timer form and not creating one. We’ve added appropriate default values for each. Now for
ToggleableTimerForm:
time-tracking/components/ToggleableTimerForm.js
export default class ToggleableTimerForm extends Component {
static propTypes = {
onFormSubmit: PropTypes.func.isRequired,
};
This component only takes a single required prop, onFormSubmit, which fires when we submit our
timer form.
Methodology review
While building our time-tracking app, we learned and applied a methodology for building React
apps. Again, those steps were:
1. Break the app into components
We mapped out the component structure of our app by examining the app’s working UI. We
then applied the single-responsibility principle to break components down so that each had
minimal viable functionality.
2. Build a static version of the app
Our bottom-level (user-visible) components rendered JSX based on static props, passed down
from parents.
React Fundamentals 133
3. Determine what should be stateful
We used a series of questions to deduce what data should be stateful. This data was represented
in our static app as props.
4. Determine in which component each piece of state should live
We used another series of questions to determine which component should own each piece of
state. App owned timer state data and ToggleableTimerForm and EditableTimer both held state
pertaining to whether or not to render a TimerForm.
5. Hardcode initial states
For the components that own state, we initialized state properties with hardcoded values.
6. Add inverse data flow
We added interactivity by decorating buttons with onPress handlers. These called functions
that were passed in as props down the hierarchy from whichever component owned the
relevant state being manipulated.
If we were planning to add server communication to our application, it would make sense to do it
now given we’ve completed setting up the base of our entire application.
Up next
With the first chapter, we explored the basics of React Native by building a weather app. In this
chapter, we dove deeper into the fundamentals of the React API by creating a more interactive
application with more components. Although we covered a number of important concepts including
a useful pattern for building React Native apps from scratch, we’ve so far only briefly covered each
of React Native’s built-in components, like View and Text. Over the next two chapters, we’ll examine
a number of React Native’s core components in greater detail.
Core Components, Part 1
What are components?
Components are the building blocks of any React Native application. We used components like View
and Text throughout the previous chapters to create the UI for our weather app and our timer app.
Out-of-the-box, React Native includes components for everything from form controls to rich media.
Up to this point, we’ve been using React Native components without fully exploring how they
work. In this chapter, we’ll study the most common built-in React Native components. Just as in
the previous chapters, we’ll build an application as we go. When we come across a new topic, we’ll
deep dive into that topic before we keep building. At the end of the chapter, you should have a solid
foundation of knowledge for using any React Native component even the ones we don’t cover will
follow many of the same patterns.
UI abstraction
Components are an abstraction layer on top of the underlying native platform. On an iOS device,
a React Native component is ultimately rendered as a UIView. On Android, the same component
would be rendered as an android.view. As React Native expands to new platforms, the same code
should be able to render correctly on more and more devices.
React Native is already supported on the universal Windows platform
55
, Apple TV (part of
the main react-native repository
56
), React VR
57
, and the web
58
.
As you start building complex apps, you’ll likely run into cases where you want to use a feature that
exists on one platform but not the other. Platform-specific components exist for cases like these.
Generally, the component’s name will end with the name of the platform e.g. NavigatorIOS. As
we mentioned in the “Getting Started”, there are several ways to run different code on different
platforms you will need to do this for platform-specific components.
Building an Instagram clone
In this chapter, we’ll use the most common React Native components to build an app that resembles
Instagram. We’ll build the main image feed with the components View, Text, Image and FlatList.
We’ll also build a comments screen using TextInput and ScrollView.
55
https://github.com/Microsoft/react-native-windows
56
https://github.com/facebook/react-native
57
https://facebook.github.io/react-vr/
58
https://github.com/necolas/react-native-web
Core Components, Part 1 135
To try the completed app on your phone:
On Android, you can scan this QR code from within the Expo app:
On iOS, you can navigate to the image-feed/ directory within our sample code folder and build
the app using the same process in previous chapters. You can either preview it using the iOS
simulator or send the link of the project URL to your device.
Our app will have two screens. The first screen is the image feed:
Core Components, Part 1 136
The second screen opens when we tap “3 comments” to display comments for that image:
Project setup
Just as we did in the previous chapters, let’s create a new app with the following command:
$ create-react-native-app image-feed --scripts-version 1.14.0
Once this finishes, navigate into the image-feed directory.
Choose one of the following to start the app:
yarn start - Start the Packager and display a QR code to open the app on your Android phone
yarn ios - Start the Packager and launch the app on the iOS simulator
yarn android - Start the Packager and launch the app on the Android emulator
You should see the default App.js file running, which looks like this:
Core Components, Part 1 137
Now’s a good time to copy over the image-feed/utils directory from the sample code into your
own project. Copy the utils directory into the image-feed directory we just created.
How we’ll work
In this chapter, we’ll build our app following the same methodology as the previous chapter. We’ll
break the app into components, build them statically, and so on. We won’t specifically call out each
step, since it isn’t necessary to follow them exactly. They’re most useful as a reference for when
you’re unsure what to do next.
If at any point you get stuck when building an app of your own, consider identifying which
steps you’ve completed, and following the steps more closely until you’re back on track.
Breaking down the feed screen
We want to start thinking about our app in terms of the different components of our UI. Ultimately
our app will render built-in components like View and Text, but as we learned in the previous chapter,
it’s useful to build higher levels of abstraction on top of these. Let’s start by figuring out how our
main image feed might break down into components.
Core Components, Part 1 138
A good component is generally concise and self-contained. By looking at the screenshot we are trying
to build, we can identify which pieces are reasonably distinct from others and reused in multiple
places. Since we’re only building a couple screens, we won’t be able to make fully informed decisions
about which parts of the screenshots are most reusable as we don’t know what the other screens
in the app will look like. But we can make some pretty good guesses. Here’s one way we can break
down the main feed:
Avatar - The profile photo or initials of the author of the image
AuthorRow - The horizontal row containing info about the author: their avatar and their name
Card - The item in the image feed containing the image and info about its author
CardList - The list of cards in the feed
Each of these build upon one another: CardList contains a list of Card components, which each
contain an AuthorRow, which contains an Avatar.
Core Components, Part 1 139
Top-down vs. bottom-up
When it comes to building the UI components of an app, there are generally two approaches: top-
down and bottom-up. In a top-down approach, we would start by building the CardList component,
and then we would build the components within the CardList, and then the components within
those, and so on until we reach the inner-most component, Avatar. In a bottom-up approach, we
would start with the innermost components like Avatar, and keep building up higher levels of
abstraction until we get to the CardList. Choosing between these two approaches is mostly personal
preference, and it’s common to do a little of both.
For this app, we’re going to work bottom-up. We’ll start with the Avatar component, and then build
the AuthorRow which uses it, and so on.
Unlike the last chapter, we’ll focus on building one component at a time, testing each one as we go.
We can modify App.js to render just the component we’re currently working on.
As an example, if we were to do this for the Avatar component, we might modify the App.js file to
render just the Avatar:
// Inside App.js
render() {
return <Avatar />;
}
We might also hardcode different kinds of props for testing:
// Inside App.js
render() {
return <Avatar initials="FL" size={35} backgroundColor={'blue'} />;
}
Isolating individual components like this is a useful technique when working with styles. A
component’s layout can change based on its parent if we build a component within a specific
parent, we may end up with styles that closely couple the parent and child. This isn’t ideal, since
we want our components to look accurate within any parent for better reusability. We can easily
ensure that components work well anywhere by building components at the top level of the view
hierarchy, since the top level has the default layout configuration.
Now that we have our strategy locked down, let’s start with the Avatar component.
Avatar
Here’s what the Avatar should look like, when rendered in isolation:
Core Components, Part 1 140
For simple apps, it’s easiest to keep all of our components together in a
components
directory.
For more advanced apps, we might create directories within components to categorize them more
specifically. Since this app is pretty simple, let’s use a flat components directory, just like we did in
the previous chapters.
Let’s create a new directory called components and create a new file within that called Avatar.js.
Our avatar component is going to render the components View and Text. It’s going to use StyleSheet,
and it’s going to validate strings, numbers, and color props with PropTypes. Let’s import these things
at the top of the file. We also have to import React.
We’ll import React in this file, even though we don’t reference it anywhere. Behind-the-
scenes, babel compiles JSX elements into calls to React.createElement, which reference the
React variable.
Add the following imports to Avatar.js:
Core Components, Part 1 141
image-feed/1/components/Avatar.js
import { ColorPropType, StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
We import ColorPropType from react-native rather than PropTypes. The PropTypes
package contains validators for primitive JavaScript types like numbers and strings. While
colors in React Native are strings, they follow a specific format that can be validated React
Native provides a handful of validators like ColorPropType for validating the contents of a
value rather than just its primitive type.
Now we can export the skeleton of our component:
image-feed/1/components/Avatar.js
export default function Avatar({ /* ... */ }) {
// ...
}
Since this component won’t need to store any local state, we’ll use the stateless functional
component style that we learned about in the previous chapter.
What should the props be for our avatar? We definitely need the initials to render. We also probably
want the size and background color to be configurable. With that in mind, we can define our
propTypes like this:
image-feed/1/components/Avatar.js
// ...
export default function Avatar({ size, backgroundColor, initials }) {
// ...
}
Avatar.propTypes = {
initials: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
backgroundColor: ColorPropType.isRequired,
};
// ...
Core Components, Part 1 142
In this app, we’ll make most of our props required using isRequired, since we’ll always pass every
prop. If we wanted to make our component more reusable, we could instead make its props optional
but it’s hard to know which props should be optional until we actually try to reuse it!
It’s time to render the contents of our Avatar. For the colored circular background, we’ll render a
View. The View is the most common and versatile component. We’ve already used it throughout the
previous chapters, but now let’s take a closer look at how it works and how to style it.
View
There are two fairly distinct things we use View for:
First, we use View for layout. A View is commonly used as a container for other components. If
we want to arrange a group of components vertically or horizontally, we will likely wrap those
components in a View.
Second, we use View for styling our app. If we want to render a simple shape like a circle or
rectangle, or if we want to render a border, a line, or a background color, we will likely use a
View.
React Native components aim to be as consistent as possible many components use similar props
as the View, such as style. Because of this, if you learn how to work with View, you can reuse that
knowledge with Text, Image, and nearly every other kind of component.
Avatar background
Let’s use View to create the circular background for our Avatar:
image-feed/1/components/Avatar.js
// ...
export default function Avatar({ size, backgroundColor, initials }) {
const style = {
width: size,
height: size,
borderRadius: size / 2,
backgroundColor,
}
return (
<View style={style} />
)
}
Core Components, Part 1 143
// ...
As we saw in previous chapters, we can use the style prop to customize the dimensions and colors
of our View component. Here, we instantiate a new object that we pass to the style prop of our
View. We can assign the size prop to the width and height attributes to specify that our View should
always be rendered as a perfect square. Adding a borderRadius that’s half the size of the width and
height will render our View as a circle. Lastly, we set the background color.
In this style object, the attributes are computed dynamically: width, height, borderRadius, and
backgroundColor are all derived from the component’s props. When we compute style objects
dynamically (i.e. when rendering our component), we define them inline this means we create
a new style object every time the component is rendered, and pass it directly to the style prop of
our component.
When there are a lot of style objects defined inline, it can clutter the render method, making the
code harder to follow. For styles which aren’t computed dynamically, we should use the StyleSheet
API. We’ll practice this more in the next few sections.
Before that, let’s make sure what we have so far is working correctly.
Try it out
Let’s add our Avatar component to App. We haven’t finished Avatar yet, but it’s useful to test as we
go in case we’ve introduced any errors.
Open up App.js and import our Avatar after our other imports:
image-feed/1/App.js
import Avatar from './components/Avatar';
Next, modify the render function to render an Avatar:
image-feed/1/App.js
// ...
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Avatar initials={'FL'} size={35} backgroundColor={'teal'} />
</View>
);
}
Core Components, Part 1 144
}
// ...
For any props we didn’t include, the Avatar will use its defaultProps. We should see a 35px teal
circle in the center of the screen:
Regardless of the size of your screen, the teal circle will render in the center. This means React
Native is calculating the center of the screen, calculating the dimensions of the Avatar, and using
these calculations to properly position the View component. As we learned in the “Getting Started”
chapter, the React Native layout engine is based on the flexbox algorithm. Let’s start digging into
how layout works: how does React Native know the dimensions for each component and where to
render it on the screen?
Dimensions
The first thing we want to think about when understanding the layout of a screen is the dimensions
of each component. A component must have both a non-zero width and height in order to render
anything on the screen. If the width is 0, then nothing will render on the screen, no matter how large
the height is.
Core Components, Part 1 145
In our Avatar example, we rendered our View with fixed dimensions by specifying an exact width
and height as part of the style prop. This is the simplest way to specify component dimensions.
Our View will always render at exactly 35px by 35px, regardless of the screen size or the components
within it.
In the case of Avatar, this is exactly the behavior we want. However, in many cases we want our
layout to automatically adapt to different screen sizes.
Our App renders a View that fills the entire screen, yet we never specified a fixed width or height. If
you look at the StyleSheet.create call at the bottom of App.js, you’ll see the style attribute flex:
1.
image-feed/1/App.js
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
We can use flex to adapt our layout to different screen sizes.
Flex
The flex style attribute gives us the ability to define layouts that can expand and shrink automat-
ically based on screen size. The flex value is a number that represents the ratio of space that a
component should take up, relative to its siblings.
If a component has no siblings, as in the case of the top-level View rendered by App, things are
straightforward:
with a flex of 1, the component will expand to fill its parent entirely
with a flex value of 0, the component will shrink to the minimum space possible (just large
enough for the component’s children to be visible, if it has any)
Since the View in App has a flex value of 1, it expands to fill its parent, which in this case is the
entire screen. Now we know why this View expands to fill the screen, but how does React Native
know to render the Avatar (and its underlying View) in the center of the screen?
Core Components, Part 1 146
Layout
We can apply three style attributes to a parent component in order to specify the layout of its
children. That is, we can specify where children render within a parent. The attributes are:
flexDirection
justifyContent
alignItems
With these attributes, we can achieve nearly any kind of layout.
flexDirection
The first attribute is flexDirection. The flexDirection we choose defines the primary axis. Children
components are laid out along the primary axis. The orthogonal axis is called the secondary axis.
The possible values for flexDirection are:
column: for a vertical layout (the default)
row: for a horizontal layout
column-reverse: the same as column but flipped vertically
row-reverse: the same as row but flipped horizontally
Core Components, Part 1 147
The names are a little confusing at first, because when you hear “row”, you might think we’ll
get a layout with multiple rows but in fact, this is saying that our layout is a row.
justifyContent
Next we’ll use the justifyContent attribute to distribute children along the primary axis. The
possible values are:
flex-start: Distribute children at the start of the primary axis (the default)
flex-center: Distribute children at the center of the primary axis
flex-end: Distribute children at the end of the primary axis
space-around: Distribute children evenly, including space at the edges
space-between: Distribute children evenly, without any space at the edges
The following diagram depicts the possible values for justifyContent in both row and column
layouts. Remember, the flexDirection sets the primary and orthogonal axes, so our choice of
flexDirection will determine the meaning of justifyContent.
Core Components, Part 1 148
alignItems
Lastly, we’ll use the alignItems attribute to align children along the secondary axis. Possible values
are:
flex-start: Align children at the start (the default)
flex-center: Align children at the center
flex-end: Align children at the end
stretch: Stretch children to fill the entire width/height of the secondary axis
The following diagram depicts the possible values for alignItems, in both row and column layouts.
Core Components, Part 1 149
Take another look at the container style in StyleSheet.create at the bottom of App.js:
image-feed/1/App.js
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
Let’s figure out how this style centers the Avatar within the View. This style object doesn’t contain
a value for the flexDirection attribute, so instead it’ll use the default value, column. This means the
Core Components, Part 1 150
primary axis is the vertical axis and the secondary axis is the horizontal axis. The justifyContent:
'center' distributes the Avatar to the center of the vertical axis. The alignItems: 'center' aligns
the Avatar in the center of the horizontal axis.
Flex and the primary axis
Now that we know how flexDirection and axes work, let’s revisit how this top-level View uses flex
to fill the entire screen.
The flex attribute of a component determines only its dimension along the primary axis. This means
that, just like for justifyContent and alignItems, we need to know what the flexDirection is in
order to use flex correctly.
In the case of our View in App, it’s best to imagine this View is actually the child of another wrapper
View that fills the entire screen. This wrapper View has the default style attributes for flexDirection,
justifyContent, and alignItems. In other words, the top-level View that we render is actually inside
a parent with style:
{
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'stretch'
}
Our top-level
View
has a fullscreen
height
because we specify
flex: 1
, which stretches it across
the vertical axis. It has a fullscreen width because its parent uses alignItems: 'stretch', which
stretches the View across the horizontal axis.
What to do if a component doesn’t show up
Beginners and experts alike frequently run into the problem where a component doesn’t render
anything on the screen. The most common reason for this is that the component has dimensions
equal to 0.
When using flex: 0 or no flex attribute, a component will only have a dimension greater than
0 along the primary axis if given explicitly (using a width or height attribute) or if its children
have dimensions greater than 0. Similarly, when using alignItems: 'stretch', a child will only
have dimensions greater than 0 along the secondary axis if given explicitly or if the parent has
dimensions greater than 0.
Thus, when a component doesn’t show up on the screen, the first thing we should do is pass an
explicit width and height style attribute (and also a backgroundColor, just to make sure something
is visible). Once a component appears on the screen, we can start understanding the component
hierarchy and evaluating how to tweak our styles.
Core Components, Part 1 151
StyleSheet
Now that we have a better understanding of layouts, lets get back to creating our Avatar component.
As a reminder, here’s what we’re aiming for:
The next thing we’ll want to add is the text within the circular View. The text should be centered,
which we now know how to do using justifyContent and alignItems. Let’s do that now.
We previously used an inline style object for the style prop of View. For styles which don’t need
to be computed dynamically based on props, such as centering the content within the View, we
generally use a StyleSheet at the bottom of the file. Let’s go ahead and update Avatar.js with the
following:
Core Components, Part 1 152
image-feed/1/components/Avatar.js
// ...
export default function Avatar({ size, backgroundColor, initials }) {
const style = {
width: size,
height: size,
borderRadius: size / 2,
backgroundColor,
};
return (
<View style={[styles.container, style]} />
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
});
In React Native, styles are most often defined below the component code in the same
file. When reading a file, generally the component is the primary concern, and styles are
secondary this is why we put the component code first. This works because the variable
name styles is hoisted to the top of the file, and the code which defines its value is executed
before the code that accesses its value. As you may have noticed, we’ve been doing this since
the start of the book, and we’ll continue to do it throughout.
In this case, we always want the text to be centered on both axes, so we’ll use justifyContent:
'center' and alignItems: 'center'. As we saw in the “Getting Started” chapter, we can merge
both of our style objects together by passing an array as the style prop of View.
When centering a single child component within a View like this, any flexDirection will
result in the same layout, so we can use either.
Now that we’ve centered the contents of our View, let’s add the text for the initials. We’ll use Text
for this. We’ve used Text before, but let’s take a step back and look at it in-depth before we add it
to our avatar.
Core Components, Part 1 153
Text
We use the Text component to render text on the screen. Text can be styled with font-specific
attributes such as fontSize. It can use nearly all of the same styles as View, such as backgroundColor
and width. However, Text has some key differences when it comes to layout.
Text dimensions
Unlike the View component, Text components have an intrinsic size. In other words, if we don’t
specify a width or height, a Text component will still show up on the screen. If we were to put a
background color behind it to visualize the width and height, we could see that the background is
exactly the size of the text we see (plus or minus a little space, depending on the line height).
Rendering a Text component with:
<Text style={{ backgroundColor: 'red' }}>
Hello World
</Text>
Gives us:
Core Components, Part 1 154
Specifying a width, height, or flex attribute as part of the style will override the intrinsic dimensions
of the Text. Rendering a Text component with:
<Text style={{ backgroundColor: 'red', width: 60, height: 60 }}>
Hello World
</Text>
Gives us:
Text context will automatically wrap around by default when it fills the width of the component.
This is configurable with the numberOfLines prop.
Common Text props and styles
Here are a few common style attributes that you might want to use with text:
color - A string representing the color of the text.
fontFamily - A string with the name of the font family (this font family must already exist on
the device).
fontSize - A number value equal to the size of the font in points.
fontStyle - Either 'normal' or 'italic'.
Core Components, Part 1 155
fontWeight - The thickness of each character. One of 'normal', 'bold', '100', '200', '300',
'400', '500', '600', '700', '800', or '900'. If the chosen weight isn’t available on the device,
the nearest available weight will be used instead).
textAlign - The text alignment. One of 'left', 'right', 'center', 'justify' (iOS only),
'auto'.
In addition, we use the following props frequently:
numberOfLines - The number of lines to allow before truncating the text.
ellipsizeMode - How text should be truncated when it exceeds numberOfLines. One of 'head',
'middle', 'tail', 'clip' (iOS only).
You can find the full list of props and styles for Text in the official docs
59
.
Like View elements, Text elements can have children. This is useful when you want to have multiple
styles of text within the same paragraph. The Text element will inherit styles from its parent. If the
parent has a fontSize of 16 and color of blue, a child Text element will have the same styles by
default. The child Text element can be styled to override its parent’s styles as needed.
Adding Text to Avatar
Let’s render and style a Text component to display the initials in the Avatar component:
image-feed/1/components/Avatar.js
// ...
export default function Avatar({ size, backgroundColor, initials }) {
const style = {
width: size,
height: size,
borderRadius: size / 2,
backgroundColor,
}
return (
<View style={[styles.container, style]}>
<Text style={styles.text}>{initials}</Text>
</View>
);
}
59
https://facebook.github.io/react-native/docs/text.html
Core Components, Part 1 156
// ...
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
text: {
color: 'white',
},
});
After saving Avatar.js, you should see the following:
Since we don’t want our content centered on the screen, let’s update the styles in App.js to
render content starting at the top left. We can do this by removing alignItem and justifyContent.
Since we’re in a View with flexDirection: "column" (the default), we can use justifyContent:
"flex-start" (also the default) to distribute content starting at the top of the screen.
We also want to leave room at the top of the screen for the status bar. We’ll import Constants from
expo so we can use Constants.statusBarHeight.
Core Components, Part 1 157
The expo library provides a variety of APIs and components beyond those pro-
vided by react-native – these are available to us out-of-the-box since we’re using
create-react-native-app to create our app.
Add the following import to the top of App.js:
image-feed/1/App.js
import { Constants } from 'expo';
Then update the container style at the bottom of the file to:
image-feed/1/App.js
// ...
const styles = StyleSheet.create({
container: {
marginTop: Constants.statusBarHeight,
flex: 1,
backgroundColor: '#fff',
},
});
Now we should see our avatar at the top left of the screen, sitting just below the status bar.
Core Components, Part 1 158
In order to create the
Avatar
, we covered
View
,
Text
,
StyleSheet
, and layout with flexbox. These are
the built-in components and APIs required to build nearly any custom component in React Native.
We’ll spend most of the rest of the chapter using them to build other components in our image feed
app.
AuthorRow
Now that we’ve completed the Avatar, let’s move on to the next component! Let’s create the
horizontal row containing our Avatar and the full name of the photo author.
Core Components, Part 1 159
Create a new file
AuthorRow.js
in our
components
directory.
In this file, we’ll import mostly things we’ve seen already: StyleSheet, View, Text, PropTypes, and
React. We’ll also import a TouchableOpacity so that we can handle taps on the “Comments” text to
take us to the comments screen. We’ll also need to import the Avatar component we just made, and
a few of the utility functions we copied into this project at the start of the chapter.
If you haven’t copied over the utils directory from our sample code, you should do so now.
image-feed/1/components/AuthorRow.js
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
import Avatar from './Avatar';
import getAvatarColor from '../utils/getAvatarColor';
import getInitials from '../utils/getInitials';
Now let’s figure out the propTypes for the component. We’ll want to configure the full name we
Core Components, Part 1 160
display next to the Avatar and the text we use for the “Comments” link on the right side. We’ll also
want to propagate press events when the user taps the link.
image-feed/1/components/AuthorRow.js
// ...
export default function AuthorRow({ fullname, linkText, onPressLinkText }) {
}
AuthorRow.propTypes = {
fullname: PropTypes.string.isRequired,
linkText: PropTypes.string.isRequired,
onPressLinkText: PropTypes.func.isRequired,
};
// ...
Thinking about the layout of the component, we’ll want to have a View with flexDirection: 'row'.
Within this we’ll render an Avatar, a Text, and a TouchableOpacity.
Let’s start with the styles for the View and Text. Add this to the bottom of the file:
image-feed/1/components/AuthorRow.js
// ...
const styles = StyleSheet.create({
container: {
height: 50,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 10,
},
text: {
flex: 1,
marginHorizontal: 6,
},
});
We use flex: 1 so that Text expands to fill any remaining space in the View. This will push the
TouchableOpacity to the right side.
Now we can fill out the component function:
Core Components, Part 1 161
image-feed/1/components/AuthorRow.js
// ...
export default function AuthorRow({ fullname, linkText, onPressLinkText }) {
return (
<View style={styles.container}>
<Avatar
size={35}
initials={getInitials(fullname)}
backgroundColor={getAvatarColor(fullname)}
/>
<Text style={styles.text} numberOfLines={1}>
{fullname}
</Text>
{/* ... */}
</View>
);
}
// ...
We’ll use numberOfLines={1} so that the Text is truncated when it reaches the end of the line, rather
than wrapping around to multiple lines.
Now let’s render a TouchableOpacity to add the “Comments” link text and handle taps.
TouchableOpacity
The TouchableOpacity component is similar to View, but lets us easily respond to tap gestures in a
performant way. The TouchableOpacity component fades out when pressed, and fades back in when
released. The opacity animation happens on the native side (it doesn’t trigger a re-render), so the
animation is extremely smooth and the interaction is low latency. The opacity value when pressed
can be configured with the activeOpacity prop by providing a number from 0 to 1.
If you don’t like the opacity animation, you can instead use a TouchableHighlight for a background
color changing animation.
One minor inconvenience with both TouchableOpacity and TouchableHighlight: these components
can only have a single child element, so if we want multiple children, we will need to wrap them in
a View.
Core Components, Part 1 162
Adding TouchableOpacity to AuthorRow
Let’s render a TouchableOpacity for “Comments” to the right of the Text in our AuthorRow. We’ll
use the onPress prop of the TouchableOpacity to call our onPressLinkText prop.
image-feed/1/components/AuthorRow.js
export default function AuthorRow({ fullname, linkText, onPressLinkText }) {
return (
<View style={styles.container}>
<Avatar
size={35}
initials={getInitials(fullname)}
backgroundColor={getAvatarColor(fullname)}
/>
<Text style={styles.text} numberOfLines={1}>
{fullname}
</Text>
{!!linkText && (
<TouchableOpacity onPress={onPressLinkText}>
<Text numberOfLines={1}>{linkText}</Text>
</TouchableOpacity>
)}
</View>
);
}
We use !!linkText to conditionally render the <TouchableOpacity> element. The double negation
with !! lets us make sure we’re dealing with a boolean value.
Since linkText is a string, the && expression would evaluate to a string type when linkText
is the empty string '' in React Native (unlike on the web), we’re not allowed to render
string values outside of Text (even empty strings).
Try it out
Let’s update App.js to render our AuthorRow component in order to test it:
Core Components, Part 1 163
image-feed/1/App.js
// ...
import AuthorRow from './components/AuthorRow';
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<AuthorRow
fullname={'First Last'}
linkText={'Comments'}
onPressLinkText={() => {
console.log('Pressed link!');
}}
/>
</View>
);
}
}
// ...
Here’s what our AuthorRow should look like:
Core Components, Part 1 164
You can also try pressing the “Comments” text. The text’s opacity should animate and you’ll see
“Pressed link!” logged to the terminal.
In our AuthorRow, as in earlier chapters, we used the style attributes paddingHorizontal and
marginHorizontal to adjust the spacing between the different components we rendered. Let’s dive
into how these attributes work.
Padding, margin, borders, and the box model
The React Native layout engine uses what’s known as the box model for customizing spacing. You
might be familiar with the box model if you’ve developed for the web. There are three main style
attributes we can use:
margin: This is the amount of space between a component and its siblings or the edge of its
parent’s content area.
border: This is the border drawn around the component, which can vary in width, style (e.g.
a dashed line), and color.
padding: this is the spacing within a component before its children components.
Each of these style attributes can have a different size on each side of the component: top, right,
bottom, and left. For example, if we wanted to set a top margin of 10 pixels, we would write
Core Components, Part 1 165
marginTop: 10 in the styles object. For convenience, we can set all four sides to have the same
size with margin: 10. We can also set vertical and horizontal margin with marginVertical: 10 and
marginHorizontal: 10. The more specific style attributes will override the more generic ones if
we write margin: 10, marginTop: 20, the top will have a margin of 20, while the rest of the sides
will have a margin of 10. All of the same rules apply to padding and border too (except that for
border, the attribute is called borderWidth instead of border).
Here is an illustration of the box model:
In this example, there’s a margin of 20, a borderWidth of 10, padding of 20, and a content area of
130 wide by 80 tall. The borderWidth and margin on the bottom side are different than the rest: the
border on the bottom is 20, and the margin on the bottom is 30. Here’s how we might write the style
for this:
{
margin: 20,
borderWidth: 10,
padding: 20,
borderBottomWidth: 20
marginBottom: 30
};
When we use a fixed width or height, this includes the content area, the padding area, and the border
width. Margin is not counted in the width or height, since it is space outside of the component’s
edges. The width in this example is 10 + 20 + 130 + 20 + 10 = 190, and the height is 10 + 20 +
80 + 20 + 20 = 150.
Core Components, Part 1 166
We’ll continue using these spacing style attributes and the box model as we build the rest of the
components in our app.
Card
Next up, we’ll make the card containing AuthorRow and the Image component.
Since rendering Image components will be an important part of our app, let’s look at how images
work in more detail.
Image
We use the Image component to render images on the screen. There are two ways to include images
in an app: we can bundle an image asset with our code (which will then get stored on the device),
or we can download an image from a URI.
Bundling image assets
To bundle an image asset, we can require the image by name from our project directory just like
any other file. The React Native packager will give us a reference to this image (a number) that
Core Components, Part 1 167
represents the image’s metadata. The packager will automatically bundle images for multiple pixel
densities if we name them with the @ suffix: .png for standard resolution, @2x.png for 2x resolution,
and @3x.png for 3x resolution. We can pass an image reference to the source prop of an Image to
render it.
For example, if we had a file called foo.png in the root directory of our app, we could use:
<Image source={require('./foo.png')} />
We won’t bundle any images in this app, however, since the images we want to display come from
the web. Instead, we’ll load remote images assets.
Remote image assets
To display an image from a URI, we must pass an object to the source prop of the Image component.
The object should contain a string value uri, and may optionally contain number values for width
and height (representing the image’s intrinsic dimensions, pre-calculated). The Image component
will automatically download the data from the URI and display it once loaded.
<Image source={{ uri: 'https://unsplash.it/600/600' }} />
Since large images may take a while to download, we’ll often show a loading indicator of some sort
before the download has finished. We can pass a callback function to the onLoad prop of Image to
determine when the image has loaded. We’ll explore this shortly.
In this chapter, we’ll use the open API, https://unsplash.it, to fetch images for our image feed.
This API is very useful for testing apps that need placeholder images. We’ll use two API endpoints:
https://unsplash.it/${width}/${height}: This endpoint gives us a random image. We can
use the query parameter image and pass the id of an image to fetch a specific image, e.g.
?image=10. We can specify the dimensions of the image by putting the desired width and
height in the URL. We’ll choose an arbitrary size, 600 x 600, for this app.
https://unsplash.it/list: This endpoint gives us a list of image metadata objects. The
metadata object for each image contains an id which we can use for the image query parameter
of the previous endpoint.
We’ll be using two utility functions in utils/api.js: getImageFromId(id) and fetchImages().
These functions correspond to the two unsplash.it APIs, respectively.
Core Components, Part 1 168
Common image styles
We can use the resizeMode style (or prop both work) to determine how the image is cropped in
the case where the image data’s intrinsic dimensions are different than the dimensions of the Image
component.
The options for resizeMode are:
cover: The image scales uniformly to fill the Image component. The image will be cropped by
the bounding box of the component if they have different aspect ratios.
contain: The image scales uniformly to fit within the component. The component’s background
color will show if they have different aspect ratios.
stretch: The image stretches to fill the component.
repeat: The image repeats itself at its intrinsic dimensions to fill the component (iOS-only).
center: The image maintains its intrinsic dimensions, and is centered within the component.
We can use the aspectRatio style to render the image at a specific aspect ratio, regardless of its
intrinsic dimensions. We provide a number value which represents the ratio of width to height. For
example, if we set aspectRatio: 2, this means the ratio of width to height is 2 to 1 the image will
render twice as wide as it is tall.
While most commonly used with images, the aspectRatio style can be used on any component,
such as View or Text.
If you’re coming from the web, you’ll likely find this style surprising since there’s no
equivalent style in CSS. The React Native layout engine, Yoga, added this style to its flexbox
implementation.
Yoga
React Native uses the Yoga layout engine (also from Facebook). This is a cross-platform implemen-
tation of the flexbox algorithm. It matches the algorithm used by web browsers pretty closely, but
with two important differences:
The default values are different
Yoga adds a couple new features that don’t exist in the browser (like aspectRatio)
If you want to read more about the algorithm and all of the styles available to use, check out the
Yoga docs
a
.
a
https://facebook.github.io/yoga/
Core Components, Part 1 169
Adding Image to Card
Let’s set up the outline for our Card component and render an Image.
Create a new file Card.js in the components directory. Add the following to this file:
image-feed/1/components/Card.js
import { Image, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
import AuthorRow from './AuthorRow';
export default class Card extends React.Component {
static propTypes = {
fullname: PropTypes.string.isRequired,
image: Image.propTypes.source.isRequired,
linkText: PropTypes.string,
onPressLinkText: PropTypes.func,
};
static defaultProps = {
linkText: '',
onPressLinkText: () => {},
};
// ...
render() {
// ...
}
}
Most of the props we use here should look familiar: fullname, linkText, and onPressLinkText
will all get passed into the AuthorRow we created earlier. The interesting one is image we use
Image.propTypes.source as the type, so that we can pass this directly into the source prop of the
Image we’ll render.
Let’s fill out the component function:
Core Components, Part 1 170
image-feed/1/components/Card.js
// ...
render() {
const { fullname, image, linkText, onPressLinkText } = this.props;
return (
<View>
<AuthorRow
fullname={fullname}
linkText={linkText}
onPressLinkText={onPressLinkText}
/>
<Image style={styles.image} source={image} />
</View>
);
}
// ...
const styles = StyleSheet.create({
image: {
aspectRatio: 1,
backgroundColor: 'rgba(0,0,0,0.02)',
},
});
We’ll render a View (with the default style flexDirection: 'column') in order to vertically stack
our AuthorRow and Image component. Since the View style defaults to alignItems: 'stretch', the
image stretches horizontally to fill the screen. We use aspectRatio: 1 to make the height of the
Image match its full-screen width, rendering as a perfect square. We put a backgroundColor on the
Image which will show before the image loads, or behind the image if the image is transparent.
Try it out
To test this, let’s render our new Card from App. Update the component in App.js to the following:
Core Components, Part 1 171
image-feed/1/App.js
// ...
import Card from './components/Card';
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Card
fullname={'First Last'}
linkText={'Comments'}
onPressLinkText={() => {
console.log('Pressed link!');
}}
image={{ uri: 'https://unsplash.it/600/600' }}
/>
</View>
);
}
}
// ...
When you save App.js, you should see our AuthorRow from earlier plus a random image on your
device:
Core Components, Part 1 172
Loading status
You might notice that we see the background color behind our image as we wait for it to load. Let’s
add a loading indicator before the image has fully loaded, to provide more feedback to the user.
We can pass a callback to the onLoad prop of Image in order to monitor the loading status. Let’s keep
track of the Image loading status in the state of our Card component. Update Card.js to include the
following:
image-feed/1/components/Card.js
export default class Card extends React.Component {
// ...
state = {
loading: true,
};
handleLoad = () => {
this.setState({ loading: false });
};
Core Components, Part 1 173
render() {
const { fullname, image, linkText, onPressLinkText } = this.props;
const { loading } = this.state;
return (
<View>
<AuthorRow
fullname={fullname}
linkText={linkText}
onPressLinkText={onPressLinkText}
/>
<Image style={styles.image} source={image} onLoad={this.handleLoad} />
</View>
);
}
}
Now we’re tracking when the image has fully loaded with state.loaded. Next we’ll render the
loading indicator when state.loaded is true.
ActivityIndicator
We can render a loading indicator using the ActivityIndicator component.
Core Components, Part 1 174
This component accepts all the same props as View, plus a few more:
animating: A bool indicating whether to show or hide the indicator (defaults to true).
color: The color of the spinner (defaults to gray).
size: One of 'small' or 'large' (defaults to small).
We want to position the ActivityIndicator in the center of the image. Unlike View, the Image
component doesn’t accept a children prop, so we can’t put the ActivityIndicator inside it. We
could use the ImageBackground component like we did in the “Getting Started” chapter, but let’s
instead look at a more generic way we can stack components: position.
Position
So far we’ve mostly used the style attributes flexDirection, justifyContent, and alignItems in our
layouts. React Native gives us another powerful style attribute we can use to adjust layout: position.
Position can be either 'relative' or 'absolute'.
relative
When set to 'relative' (which is the default), we’re able to tweak the position of a component after
it has already been laid out according to its flex, width, height, etc. We can use a combination of
Core Components, Part 1 175
top, right, bottom, and left. For example, if we want to move a component down on the screen by
20 pixels, we could say top: 20 to indicate that its top should be 20 pixels greater than it is currently.
Unlike specifying padding, margin, or borderWidth, the style attributes like top don’t affect other
elements in the layout. In other words, adding top: 20 will change the position of the element it
was applied to, but not the position of the element’s siblings or parent (even if this causes them to
overlap).
absolute
When set to 'absolute', the layout of the parent and the component’s flex style are completely
ignored. Instead we use top, right, bottom, and left to specify exactly how the component should
be placed within its parent. For example, if we want the component to be 10 pixels from the bottom
and 20 pixels from the right side of its parent, we can say bottom: 20, right: 10. As always, we
need to make sure the component has dimensions greater than 0. Since flex is ignored, we may
need to specify a fixed width and height. We can also ensure the component has dimensions greater
than 0 by specifying both top and bottom, or both left and right. For example, if we say left: 10,
right: 10, the component will stretch horizontally to fill the space from 10 pixels from the left of its
parent to 10 pixels from the right. Components positioned with position: 'absolute' don’t affect
the layout of their siblings or parent components.
It’s common to use position: 'absolute' to make elements overlap. Suppose we want two sibling
elements to overlap, filling their parent completely. We can add a style like this to both siblings:
const absoluteFillStyle = {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
};
This style will cause an element to fill its parent completely, since its top will match its parent’s top,
its right side will match its parent’s right side, and so on. This technique is so common that there’s a
predefined style to do the same thing: StyleSheet.absoluteFill. This value can be passed directly
to the style prop of an element. Alternately, you can use ...StyleSheet.absoluteFillObject,
copying each of these properties into another style this is useful if you want to override one
or two properties but keep the rest.
This overlapping behavior is exactly what we want for positioning our ActivityIndicator in the
center of our Image.
Adding ActivityIndicator to Card
We’ll use StyleSheet.absoluteFill to ensure that our ActivityIndicator matches the dimensions
of our Image. To do this, we’ll need to create a common ancestor View for both.
Core Components, Part 1 176
First, in Card.js, make sure to import ActivityIndicator:
image-feed/1/components/Card.js
import { ActivityIndicator, Image, StyleSheet, View } from 'react-native';
Then, update the render method of Card to the following:
image-feed/1/components/Card.js
render() {
const { fullname, image, linkText, onPressLinkText } = this.props;
const { loading } = this.state;
return (
<View>
<AuthorRow
fullname={fullname}
linkText={linkText}
onPressLinkText={onPressLinkText}
/>
<View style={styles.image}>
{loading && (
<ActivityIndicator style={StyleSheet.absoluteFill} size={'large'} />
)}
<Image
style={StyleSheet.absoluteFill}
source={image}
onLoad={this.handleLoad}
/>
</View>
</View>
);
}
If you save Card.js and look at your device, you should see the ActivityIndicator positioned in
the center of the Image before it loads.
It can be a little confusing to see how this works at first, so let’s walk through it. We moved the
styles.image style to the inner View that’s the parent of the Image. This inner View is inside a
parent View with alignItems: 'stretch' (the default) and it has aspectRatio: 1, so we know the
inner View will have the same width as its parent (in this case, the width of the screen) and a height
equal to its width. The Image and ActivityIndicator will have the same top, right, bottom, and
left of the inner View in other words, they’ll will match the dimensions of the View.
Core Components, Part 1 177
The order we render components in our code matters here: within the inner View, we render the
ActivityIndicator before the Image. The component rendered last in the code will render on top of
its siblings visually. This normally isn’t something we have to think about, since components don’t
stack on top of one another. With position however, sibling components may overlap, and the order
we render them determines their order on screen. In this case, our image Image renders on top of
our ActivityIndicator. The onLoad event may get called after the image has been drawn, so this
way the Image covers up the ActivityIndicator even if both are rendered at the same time.
If you’re coming from the web, you may be wondering about the zIndex style attribute.
React Native has a attribute zIndex, but it behaves a little differently and can have
somewhat unpredictable effects. It’s generally safer to render components in the correct
order, if possible.
There are many other ways to achieve the same layout. Another approach would be to set the Image
style to flex: 1 to fill the View completely.
<View style={styles.image}>
{loading && (
<ActivityIndicator style={StyleSheet.absoluteFill} size={'large'} />
)}
<Image
style={{ flex: 1 }}
source={image}
onLoad={this.handleLoad}
/>
</View>
We could also leave styles.image on the Image, and rely on the fact that its parent View will resize
along the vertical axis to contain its children:
<View>
{loading && (
<ActivityIndicator style={StyleSheet.absoluteFill} size={'large'} />
)}
<Image
style={styles.image}
source={image}
onLoad={this.handleLoad}
/>
</View>
All of these approaches are reasonable, so deciding which to use mostly comes down to preference.
We chose our approach for this chapter because it’s easier to understand at first glance: both children
Core Components, Part 1 178
of the View have StyleSheet.absoluteFill, so it’s clear that they will overlap completely just by
reading the code.
Now that we have our Card component, we can render a list of these to create the main feed.
CardList
The CardList component will render the infinitely scrolling list of authors and images.
We’ll render this list of cards using the FlatList component.
FlatList
FlatList components are used for rendering large quantities of scrollable content. Instead of
rendering a children prop, the FlatList renders each item in an input data array using the
renderItem prop. The renderItem prop is a function which takes an item from the data array and
maps it to a React Element. Each item in data should be an object with a unique id, so that React
can determine when items have been rearranged.
Core Components, Part 1 179
The FlatList is generally performant: it only renders the content on screen (clipping offscreen
content), and only updates rows that have changed. The FlatList is built using a more generic
component, the ScrollView, which we’ll use later in the chapter.
Adding FlatList to CardList
Let’s create a new file, CardList.js, in our components directory. We’ll import the FlatList, our
Card, a utility for building an image url from an id, and a few other things at the top of the file:
image-feed/1/components/CardList.js
import { FlatList } from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
import { getImageFromId } from '../utils/api';
import Card from './Card';
Ultimately we’ll use https://unsplash.it to fetch the data for our feed, but for now let’s pretend
our data looks like:
[
{ id: 0, author: "Bob Ross" },
{ id: 1, author: "Chuck Norris" }
];
It’s important that the id field is unique, since we’ll use it to determine the identity of each card in
the feed. If it weren’t unique, we would start to see quirky behavior where some items don’t render.
Fortunately the API we’ll call later has unique id values (as most APIs should!).
We’ll need to provide a function to the FlatList which maps each element in our data array to its
unique key. Let’s define a utility function to do this at the top of the file, which we’ll then pass to the
FlatList as the keyExtractor prop. Our function, keyExtractor, will take an item from our array
and return the id for that item as a string. Define this function below the imports:
image-feed/1/components/CardList.js
const keyExtractor = ({ id }) => id.toString();
We’ll use this function in a moment when we render the FlatList.
Moving on to the propTypes, we’ll want to ensure our input data matches the format we defined
above. We can use a combination of PropTypes.arrayOf and PropTypes.shape. Let’s set up our
component skeleton as follows:
Core Components, Part 1 180
image-feed/1/components/CardList.js
// ...
export default class CardList extends React.Component {
static propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
author: PropTypes.string.isRequired,
}),
).isRequired,
};
render() {
// ...
}
}
We’ll use a class component instead of a functional component, since we’ll add a few methods which
need to access props when we add commenting later.
So far we’ve seen how we can validate primitive values with PropTypes.bool, PropTypes.string,
etc. We can use PropTypes.shape() to validate an object, passing the keys of the values we want to
validate. We can use PropTypes.array() to validate an array, passing the type of the element.
If we were planning to use this items data structure in multiple places, we might want to define its
type in a separate file, such as ItemsPropType.js. That way we can define it once and import it from
multiple files, rather than defining it in multiple places. React Native exports a few built-in types
this way, such as ViewPropTypes.
Now let’s render the FlatList:
Core Components, Part 1 181
image-feed/1/components/CardList.js
renderItem = ({ item: { id, author } }) => (
<Card
fullname={author}
image={{
uri: getImageFromId(id),
}}
/>
);
render() {
const { items } = this.props;
return (
<FlatList
data={items}
renderItem={this.renderItem}
keyExtractor={keyExtractor}
/>
);
}
Destructuring assignments, revisited
In the previous chapter, we covered destructuring assignments. Destructuring assignments can also
be nested, as in the example above:
renderItem = ({ item: { id, author } }) => {}
This is equivalent to:
renderItem = (obj) => {
const id = obj.item.id;
const author = obj.item.author;
}
We provide the prop keyExtractor to instruct the FlatList how to uniquely identify items this
helps the FlatList determine when it needs to re-render items as they go in and out of the visible
portion of the screen.
Each time the FlatList decides to render a new item, it will call the renderItem function we provide
Core Components, Part 1 182
it with, with an object as a parameter. The object contains some rendering metadata, along with the
item from the array we passed as the data prop.
Within renderItem, we can then return a Card component based on the item. We can use the item’s
id to construct a URI for an image, leveraging our getImageFromId() utility function.
Save CardList.js and let’s test it out.
Try it out
Update App.js to render the CardList using the hardcoded data we mentioned earlier:
image-feed/1/App.js
// ...
import CardList from './components/CardList';
const items = [
{ id: 0, author: 'Bob Ross' },
{ id: 1, author: 'Chuck Norris' },
];
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<CardList items={items} />
</View>
);
}
}
// ...
Save App.js and you should see this:
Core Components, Part 1 183
Now that we’ve finished our last custom UI component for the image feed, we can use it to create
our Feed screen.
Adding a screen
Our app will have two screens:
Feed: The image feed
Comments: The list of comments for a specific image
In React Native, screens are components just like any other. However, it’s useful to think about
screens slightly differently. Screens are components that fill the entire device screen. They often
handle non-visual concerns, like fetching data and handling navigation to other screens.
It’s common to keep all of the screen components in an app together in a screens directory. For more
advanced apps, we might create directories within screens to categorize them more specifically.
Since this app is pretty simple, let’s use a flat screens directory.
Create a new directory called screens within our top level image-feed directory, and create a new
file within screens called Feed.js.
Core Components, Part 1 184
The Feed screen
This screen will fetch live data from https://unsplash.it and pass the data into our CardList. The
data we fetch will follow the same format as the hardcoded list we’re using currently.
Now that we’re fetching remote data asynchronously, we need to consider loading and error states.
This screen will show a simple loading indicator and error status.
Add the following imports to Feed.js:
image-feed/1/screens/Feed.js
import {
ActivityIndicator,
Text,
ViewPropTypes,
SafeAreaView,
} from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
import { fetchImages } from '../utils/api';
import CardList from '../components/CardList';
Often screens are configured with props just like other components. In this case, we’ll allow a style
prop, which we’ll use for the top level View within this screen. This allows a lot of flexibility for our
screen to be styled however we need. The type of this prop will be the same as the style prop of
View React Native provides this validator as a separate import, ViewPropTypes.
You might be tempted to use PropTypes.object to represent a style, but this doesn’t work
very well. Styles created with StyleSheet.create are represented as numbers, so this will
cause a warning. Also, ViewPropTypes.style provides in-depth type-checking of each key
and value, which is very valuable.
image-feed/1/screens/Feed.js
// ...
export default class Feed extends React.Component {
static propTypes = {
style: ViewPropTypes.style,
};
static defaultProps = {
style: null,
Core Components, Part 1 185
};
// ...
}
// ...
It’s common to allow a style prop for creating extremely flexible custom components. We could
technically use this style prop however we want, such as styling a deeply nested component
however, when naming a prop the same as a built-in View prop, we’ll generally try to keep the
behavior similar. Following built-in component conventions makes it easier for other developers to
understand how to use our custom components without reading the source code.
We’ll keep track of three things in the state of our Feed: loading, error, and items.
image-feed/1/screens/Feed.js
state = {
loading: true,
error: false,
items: [],
};
We can use these to decide what to render. We’ll fetch data in componentDidMount, updating
component state when we get a response.
image-feed/1/screens/Feed.js
async componentDidMount() {
try {
const items = await fetchImages();
this.setState({
loading: false,
items,
});
} catch (e) {
this.setState({
loading: false,
error: true,
});
}
}
Core Components, Part 1 186
We made componentDidMount an async function so that we can use the await syntax within it. This
means the function will return a promise. React doesn’t use the return value of componentDidMount
for anything, so this is safe.
Now, let’s update our render method to make use of this new state:
image-feed/1/screens/Feed.js
render() {
const { style } = this.props;
const { loading, error, items } = this.state;
if (loading) {
return <ActivityIndicator size="large" />;
}
if (error) {
return <Text>Error...</Text>;
}
return (
<SafeAreaView style={style}>
<CardList items={items} />
</SafeAreaView>
);
}
We’re ready to render our Feed screen from App!
Adding Feed to App
Let’s update App.js to render our new screen. First we’ll need to update the imports at the top of
the file:
image-feed/1/App.js
import { Platform, StyleSheet, View } from 'react-native';
import { Constants } from 'expo';
import React from 'react';
import Feed from './screens/Feed';
Then we can render our Feed within a wrapper View:
Core Components, Part 1 187
image-feed/1/App.js
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Feed style={styles.feed} />
</View>
);
}
}
Since our Feed uses a SafeAreaView at the top level, we’ll also need to update our styles from before.
We only want to add a marginTop on Android, or on iOS versions less than 11, since the top margin
is added automatically by the SafeAreaView on iOS 11+ now.
We can use Platform.Version to detect the native operating system version. On iOS, this is a string
like '10.3', while on Android it’s a number.
image-feed/1/App.js
const platformVersion =
Platform.OS === 'ios' ? parseInt(Platform.Version, 10) : Platform.Version;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
feed: {
flex: 1,
marginTop:
Platform.OS === 'android' || platformVersion < 11
? Constants.statusBarHeight
: 0,
},
});
And with that, we’re finished with the feed! Here’s the final result with live data:
Core Components, Part 1 188
List Performance
If you’re running a version of React Native greater than 0.55, you may see the following warning
after scrolling through a hundred or so images:
1 VirtualizedList: You have a large list that is slow to update - make sure your
2 renderItem function renders components that follow React performance best practices
3 like PureComponent, shouldComponentUpdate, etc.
React Native is letting us know that our list is taking a long time to render once it gets to a certain
length. We can address this by reducing the amount of cards we re-render.
The FlatList re-renders our cards while we scroll. Most of the time, however, a card’s data
doesn’t change, so we don’t need to update the component after the initial render the only
time the data might change is if the number of comments to display changes. We can add a
shouldComponentUpdate method to Card.js to reduce re-renders and thus improve the performance
of our FlatList. We could add the following shouldComponentUpdate method:
shouldComponentUpdate(nextProps) {
return this.props.linkText !== nextProps.linkText
}
Core Components, Part 1 189
Then our cards would only re-render if absolutely necessary. You may still get the same warning at
a certain point, but it should take several times more scrolling than before.
Most of the time using shouldComponentUpdate is a premature optimization. Even when it comes to
large lists like this, often the performance will be good enough in the production build without any
other optimizations. However, if you get a warning, it’s an optimization worth considering.
What we’ve built so far
Let’s recap what we’ve done so far. We used the components View, Text, Image, and FlatList to build
a cross-platform, infinitely scrolling list of images and authors. We created 4 components, each one
building on top of the previous: Avatar, AuthorRow, Card, and CardList. We tested each component
as we built it, by rendering from our top-level component, App. We used a variety of techniques for
layout:
Setting the width and height explicitly
Using flex to stretch elements
Using flexDirection, justifyContent, and alignItems for children layout
Using padding and margin to define spacing between elements
Using position: 'absolute' to stack elements on top of one another
Creating optimized styles with StyleSheet.create
These are the fundamental building blocks of any React Native UI. We’ll continue to use these
throughout the rest of the book, as we add more components and APIs to our repertoire.
Core Components, Part 2
Picking up where we left off
We successfully built an awesome infinitely-scrolling image feed. Next, we’re going to add a new
screen to the same app for commenting on images.
This is a code checkpoint. If you haven’t been coding along with us but would like to start
now, we’ve included a snapshot of our current progress in the sample code for this book.
If you haven’t created a project yet, you’ll need to do so with:
$ create-react-native-app image-feed --scripts-version 1.14.0
Then, copy the contents of the directory image-feed/1 from the sample code into your new
image-feed project directory.
Comments
Here’s what the comments screen will look like:
Core Components, Part 2 191
To build this portion of the app, we’ll learn how to use the
TextInput
,
ScrollView
, and
Modal
components. We’ll also cover a few other topics like AsyncStorage. We’ll make a few assumptions
so we can focus on built-in components:
we won’t use a navigation library even though we have multiple screens (more on navigation
in later chapters)
we only want to store comments locally on the device, rather than remotely via an API
comments can be saved as simple strings (no id, author, or other metadata)
the comment input field is at the top of the screen, to avoid complexities around keyboard and
scrolling (which we’ll cover in the next chapter)
there are few enough comments that a ScrollView will be performant enough (rather than
using a FlatList)
Breaking down the comments screen
The first thing we’ll want to do is break the screen down into components. Here’s one way we can
break it down:
Core Components, Part 2 192
NavigationBar - A simple navigation bar for the top of the screen with a title and a close
button
CommentInput - The input field for adding new comments
CommentList - The scrollable list of comments
The App component will be responsible for handling comment data in our app, since both the Feed
screen and Comments screen need to render this data. We’ll render the Comments screen component
from App, passing the comment data for the selected card as a prop. We’ll render the built-in Modal
component to open and close this new screen based on the state of App.
We’ll continue building bottom-up, starting with the CommentInput component, working our way
up to the screen component. We won’t test every component individually by rendering it from App
like we did in the first half of the chapter, but you’re welcome to continue to do this if you liked
having a quicker feedback loop while developing.
CommentInput
First, let’s create the input field for new comments.
Core Components, Part 2 193
TextInput
As we saw in the “Getting Started” chapter, we can use a TextInput component to create an editable
text field for the user to type in.
When working with TextInput, we’ll generally use the following props to capture user input:
value - The current text in the input field.
onChangeText - A function called each time the text changes. The new value is the first
argument.
onSubmitEditing - A function called when the user presses the return/next key to submit/move
to the next field.
It’s common to store the current text in the state of the component that renders the TextInput.
Each time the function we pass to onChangeText is called, we call setState to update the current
text. When the user presses return, the function we passed to onSubmitEditing is called we can
then perform some action with the current text, and use setState to reset the current text to the
empty string.
Core Components, Part 2 194
Common TextInput props and styles
When working with TextInput, we can use most of the same styles as Text (which includes the
styles for View). A few styles don’t work quite as well as they do on Text though: borders tend not to
render correctly, and padding and line height can conflict in unusual ways. If you’re having trouble
styling a TextInput, you may want to wrap the TextInput in a View and style the View instead.
A few other common props:
autoCapitalize - For capitalizing characters as they’re typed. One of 'none', 'sentences',
'words', 'characters'.
autoCorrect - Enable/disable auto-correct.
editable - Enable/disable the text field.
keyboardType - The type of keyboard to display. Cross-platform values are 'default',
'numeric', 'email-address', 'phone-pad'.
multiline - Allow multiple lines of input text.
placeholder - The text to show when the text field is empty
placeholderTextColor - The color of the placeholder text
returnKeyType - The text of the return key on the keyboard. Cross-platform values are 'done',
'go', 'next', 'search', 'send'.
Many more props are available in the docs for TextInput
60
Adding TextInput to CommentList
While we could render a TextInput component directly from our Comments screen, it’s often better
to create a wrapper component that encapsulates state, styles, edge cases, etc, and has a smaller
API. That’s what we’ll do in our CommentInput component. The result will be very similar to our
TextInput wrapper components from previous chapters.
Create a new file, CommentInput.js, in the components directory. We’ll import the usual React,
PropTypes, etc, along with the TextInput component:
image-feed/components/CommentInput.js
import { StyleSheet, TextInput, View } from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
We want this component to have two props:
onSubmit - we’ll call this with the comment text when the user presses the “return” key.
placeholder - a passthrough to the placeholder prop of the TextInput.
Add the following to CommentInput.js:
60
https://facebook.github.io/react-native/docs/textinput.html
Core Components, Part 2 195
image-feed/components/CommentInput.js
// ...
export default class CommentInput extends React.Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired,
placeholder: PropTypes.string,
};
static defaultProps = {
placeholder: '',
};
// ...
}
// ...
We’ll add a text value to state and methods for updating this value when the value of the TextInput
changes:
image-feed/components/CommentInput.js
state = {
text: '',
};
handleChangeText = text => {
this.setState({ text });
};
handleSubmitEditing = () => {
const { onSubmit } = this.props;
const { text } = this.state;
if (!text) return;
onSubmit(text);
this.setState({ text: '' });
};
We don’t want to allow empty comments, so when handleSubmitEditing is called, we’ll return
immediately if state.text is empty.
Core Components, Part 2 196
Last, we’ll render the TextInput. We want to add a border on the bottom, but adding borders to
TextInput can be a bit unreliable as sometimes they don’t show up. So we’ll wrap the TextInput in
a View and style the View instead:
image-feed/components/CommentInput.js
render() {
const { placeholder } = this.props;
const { text } = this.state;
return (
<View style={styles.container}>
<TextInput
style={styles.input}
value={text}
placeholder={placeholder}
underlineColorAndroid="transparent"
onChangeText={this.handleChangeText}
onSubmitEditing={this.handleSubmitEditing}
/>
</View>
);
}
image-feed/components/CommentInput.js
const styles = StyleSheet.create({
container: {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.1)',
paddingHorizontal: 20,
height: 60,
},
input: {
flex: 1,
},
});
This is where we pass our state management methods handleChangeText and handleSubmitEditing
to the TextInput, to keep track of changes to the value.
We can use StyleSheet.hairlineWidth as the border width to render the thinnest possible line on
any given device. On a retina device for example, this would be less than 1.
Core Components, Part 2 197
If you want to see what this component looks like to check your work, consider rendering
it from within App for testing.
CommentList
Next, we’ll render a list of comments for each image:
We’ll render these comments in a ScrollView. In reality, we’d probably want to use a FlatList for
performance, but let’s use a ScrollView for practice.
ScrollView
The ScrollView is simpler than the FlatList: it will render all of its children in a vertically or
horizontally scrollable list, without the additional complexity of the keyExtractor or renderItem
props.
The ScrollView is well suited for scrolling through small quantities of content (fewer than 20 items
or so). Content within a ScrollView is rendered even when it isn’t visible on the screen. For large
quantities of items, or cases where many children of the ScrollView are offscreen, you will likely
want to use a FlatList component for better performance.
Core Components, Part 2 198
ScrollView dimensions and layout
You can think of a ScrollView as two separate views, one inside the other. The outer view has a
bounded size, while the inner view can exceed the size of the outer view. If the inner view exceeds
the size of the outer view, only a portion of it will be visible. When we pass children elements to the
ScrollView, they are rendered inside this inner view. We call the inner view the “content container
view”, and can style it separately from the outer view.
Debugging a ScrollView
While building an app, it’s common to render a ScrollView but see nothing on the screen. There
are two common causes for this, based on how the outer view and the content container view work
(assuming vertical scrolling):
The content container view has flex: 0 by default, so it starts with a height of 0 and expands
to the minimum size needed to contain its children elements. If a child has flex: 1, this child
won’t be visible, since the content container has an intrinsic height of 0. While we could set
the contentContainerStyle to flex: 1, this probably isn’t what we want, since then we’ll
never have content larger than the outer view. Instead, we should make sure the children we
pass to the ScrollView have intrinsic height values greater than 0 (either by using an explicit
height, or by containing children that have height greater than 0).
The outer view does not change size based on the content container view. In addition to
ensuring that the children of a ScrollView have non-zero height, we have to make sure our
ScrollView has non-zero dimensions a fixed width and height, flex: 1 and a parent with
alignItems: stretch, or absolute positioning.
Most likely, if the ScrollView doesn’t appear, we need to add flex: 1 to each parent and to the
ScrollView itself. To debug, you can try setting a background color on each parent to see where
flex: 1 stopped getting propagated down the component hierarchy.
Adding ScrollView to CommentList
Let’s render a ScrollView that contains a list of comments. We’ll call this component CommentList.
Create a file CommentList.js in the components directory.
This component will take an items array prop of comment strings, mapping these into View and
Text elements. We’ll set up the outline for this component in CommentList.js as follows:
Core Components, Part 2 199
image-feed/components/CommentList.js
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
export default class CommentList extends React.Component {
static propTypes = {
items: PropTypes.arrayOf(PropTypes.string).isRequired,
};
// ...
}
Unlike FlatList, we don’t need to deal with the keyExtractor and data props. We can simply render
the children of the ScrollView as we would for a View:
image-feed/components/CommentList.js
renderItem = (item, index) => (
<View key={index} style={styles.comment}>
<Text>{item}</Text>
</View>
);
render() {
const { items } = this.props;
return <ScrollView>{items.map(this.renderItem)}</ScrollView>;
}
image-feed/components/CommentList.js
const styles = StyleSheet.create({
comment: {
marginLeft: 20,
paddingVertical: 20,
paddingRight: 20,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.05)',
},
});
Core Components, Part 2 200
Since comments are stored as strings, we don’t have a convenient value to use as the unique
React key. Using the comment text as the key wouldn’t work, since comments don’t have to
be unique. Using the index as the key works here, but is generally a pattern to be wary of,
since it can cause problems when rearranging items. A better solution would be to augment
our comment data with ids: we could store comments as objects, and use the uuid library
from the previous chapter to assign each comment a unique id for use as the key.
Now that we have a scrolling list of comments, we can move on to the navigation bar, which will
be the last component we make before assembling our comments screen.
NavigationBar
Since our comments screen is going to open in a modal, we want to render a navigation bar with a
title and close button.
In a real app, we would likely use a navigation library for this, but for simplicity, let’s write something
small of our own.
Create NavigationBar.js in the components directory and add the following outline:
Core Components, Part 2 201
image-feed/components/NavigationBar.js
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
export default function NavigationBar({ title, leftText, onPressLeftText }) {
// ...
}
NavigationBar.propTypes = {
title: PropTypes.string,
leftText: PropTypes.string,
onPressLeftText: PropTypes.func,
};
NavigationBar.defaultProps = {
title: '',
leftText: '',
onPressLeftText: () => {},
};
// ...
We won’t use isRequired on our props, since this component would likely be used without
some of them, e.g. leftText and onPressLeftText, if we were to add more screens to this
app.
This component will be fairly straightforward, using only concepts we’ve covered already. We’ll use
a TouchableOpacity for the close button on the left. We’ll position it with position: 'absolute',
since we don’t want the text on the left to push the title off-center (remember, using position:
'absolute' means the component no longer affects other siblings in the layout). A real navigation
library takes into account many more cases such as text on the right, icons on either side, and long
text that may bump into the title. Let’s keep things simple and just handle the one case at hand.
The component function and styles should look like this:
Core Components, Part 2 202
image-feed/components/NavigationBar.js
export default function NavigationBar({ title, leftText, onPressLeftText }) {
return (
<View style={styles.container}>
<TouchableOpacity style={styles.leftText} onPress={onPressLeftText}>
<Text>{leftText}</Text>
</TouchableOpacity>
<Text style={styles.title}>{title}</Text>
</View>
);
}
image-feed/components/NavigationBar.js
const styles = StyleSheet.create({
container: {
height: 40,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.1)',
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontWeight: '500',
},
leftText: {
position: 'absolute',
left: 20,
top: 0,
bottom: 0,
justifyContent: 'center',
},
});
Despite generally representing a numeric value, fontWeight must be a string!
We now have all of the building blocks we need: CommentInput, CommentList, and NavigationBar.
Let’s assemble them in a new screen.
Core Components, Part 2 203
Comments screen
Create a new file Comments.js within the screens directory.
Within our new screen, we’ll want to render first the NavigationBar, then the CommentInput, and
finally the CommentList. We want this screen to take 4 props:
comments - The array of comments to display.
onClose - A function prop to call when the user presses the close button.
onSubmitComment - A function prop to call when the user adds a new comment.
style - The style to apply to the top-level View of this screen (just like we did with Feed)
Add the following to Comments.js:
image-feed/screens/Comments.js
1 import { SafeAreaView, ViewPropTypes } from 'react-native';
2 import PropTypes from 'prop-types';
3 import React from 'react';
4
5 import CommentInput from '../components/CommentInput';
6 import CommentList from '../components/CommentList';
7 import NavigationBar from '../components/NavigationBar';
8
9 export default function Comments({
10 style,
11 comments,
12 onClose,
13 onSubmitComment,
14 }) {
15 return (
16 <SafeAreaView style={style}>
17 <NavigationBar
18 title="Comments"
19 leftText="Close"
20 onPressLeftText={onClose}
21 />
22 <CommentInput placeholder="Leave a comment" onSubmit={onSubmitComment} />
23 <CommentList items={comments} />
24 </SafeAreaView>
25 );
26 }
27
28 Comments.propTypes = {
Core Components, Part 2 204
29 style: ViewPropTypes.style,
30 comments: PropTypes.arrayOf(PropTypes.string).isRequired,
31 onClose: PropTypes.func.isRequired,
32 onSubmitComment: PropTypes.func.isRequired,
33 };
34
35 Comments.defaultProps = {
36 style: null,
37 };
The code for our screen is fairly simple, since we already built the different parts of the UI as
individual components.
Putting it all together
Now we need to allow navigation from the Feed screen we made earlier with this new Comments
screen.
We want the Comments screen to slide up and cover the entire screen, so we’ll use the built-in Modal
component.
Modal
The Modal component lets us transition to an entirely different screen. This is most useful for simple
apps, since for complex apps you’ll likely be using a navigation library which will come with its
own way of doing modals.
Common props include:
animationType - This controls how the modal animates in and out. One of 'none', 'slide', or
'fade' (defaults to 'none').
onRequestClose - A function called when the user taps the Android back button.
onShow - A function called after the modal is fully visible.
transparent - A bool determining whether the background of the modal is transparent.
visible - A bool determining whether the modal is visible or not.
The visible prop is the most important, since this lets us show and hide the Modal.
Core Components, Part 2 205
Adding Modal to App
We’ll maintain the state of the Modal in the state of our App component. We’ll use the visible prop of
the Modal component to show and hide it, so we’ll want to store a boolean showModal in state. We’ll
also want to store the id of the image we’re viewing comments for, so we’ll store selectedItemId
in state too. And we’ll want to store the actual text for the comments we type in, so let’s create an
object that maps from an image id to an array of comment strings. Let’s update the state in App.js
to look like this:
image-feed/App.js
state = {
commentsForItem: {},
showModal: false,
selectedItemId: null,
};
Next, we’ll make two function properties on our App component, for updating state in order to open
and close the Modal.
image-feed/App.js
openCommentScreen = id => {
this.setState({
showModal: true,
selectedItemId: id,
});
};
closeCommentScreen = () => {
this.setState({
showModal: false,
selectedItemId: null,
});
};
Notice that our openCommentScreen function takes the id of the image we want to display comments
for. We’ll need to call this function from within the CardList in order pass that id. Then we’ll
propagate the value through Feed and up to App. Save App.js and let’s head over to CardList.js to
make this possible.
Updating CardList with comments
We want the “Comments” link on each card to open the Modal we just created:
Core Components, Part 2 206
To do this, let’s tweak our CardList component, adding an onPressComments prop (which we can use
to call openCommentScreen) and a commentsForItem prop (which we can use to display the number
of comments per image).
image-feed/components/CardList.js
// ...
export default class CardList extends React.Component {
static propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
author: PropTypes.string.isRequired,
}),
).isRequired,
commentsForItem: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string))
.isRequired,
onPressComments: PropTypes.func.isRequired,
};
// ...
}
// ...
Let’s call onPressComments from within renderItem, passing the id of the item so that we know
which image to display comments for.
image-feed/components/CardList.js
renderItem = ({ item: { id, author } }) => {
const { commentsForItem, onPressComments } = this.props;
const comments = commentsForItem[id];
return (
<Card
fullname={author}
image={{
uri: getImageFromId(id),
}}
linkText={`${comments ? comments.length : 0} Comments`}
onPressLinkText={() => onPressComments(id)}
/>
Core Components, Part 2 207
);
};
There’s one small problem with our CardList so far: the count of comments we use for the linkText
won’t immediately update when we add new comments. This is due to how the FlatList decides
whether or not to re-render items; the FlatList will only re-render an item when the data prop
changes or when scrolling. In this case, we pass the items prop of CardList into the data prop of
FlatList, but our commentsForItem prop doesn’t cause the items array to change, so the FlatList
won’t update when new comments are added. We can use the prop extraData of FlatList to inform
the FlatList that it should monitor another source of input data for changes.
Let’s update the render method within CardList, passing commentsForItem as the extraData prop
of FlatList.
image-feed/components/CardList.js
// ...
const { items, commentsForItem } = this.props;
return (
<FlatList
data={items}
renderItem={this.renderItem}
keyExtractor={keyExtractor}
extraData={commentsForItem}
/>
);
// ...
Save CardList.js. This will put your app in an error state, since CardList isn’t currently being
passed a commentsForItem prop.
Core Components, Part 2 208
Let’s fix that!
Updating Feed with comments
Open Feed.js. We need to accept commentsForItem and onPressComments here too, and pass them
into CardList.
Update the propTypes:
image-feed/screens/Feed.js
static propTypes = {
style: ViewPropTypes.style,
commentsForItem: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string))
.isRequired,
onPressComments: PropTypes.func.isRequired,
};
And update the render method:
Core Components, Part 2 209
image-feed/screens/Feed.js
render() {
const { commentsForItem, onPressComments, style } = this.props;
// ..
return (
<SafeAreaView style={style}>
<CardList
items={items}
commentsForItem={commentsForItem}
onPressComments={onPressComments}
/>
</SafeAreaView>
);
}
// ...
Save Feed.js. You should still see the same error message, since we’re still not passing a value for
commentsForItem into Feed.
Updating App with comments
Let’s head back to App.js to connect these new props we’ve just added to Feed and to render the
screen.
Import the Modal component and our Comments component:
image-feed/App.js
import { Modal, Platform, StyleSheet, View } from 'react-native';
// ...
import Comments from './screens/Comments';
Then update the render method to render Comments and update Feed with new props:
Core Components, Part 2 210
image-feed/App.js
// ...
export default class App extends React.Component {
// ...
render() {
const { commentsForItem, showModal, selectedItemId } = this.state;
return (
<View style={styles.container}>
<Feed
style={styles.feed}
commentsForItem={commentsForItem}
onPressComments={this.openCommentScreen}
/>
<Modal
visible={showModal}
animationType="slide"
onRequestClose={this.closeCommentScreen}
>
<Comments
style={styles.container}
comments={commentsForItem[selectedItemId] || []}
onClose={this.closeCommentScreen}
// ...
/>
</Modal>
</View>
);
}
}
// ...
We’ll also add one new style for the comments screen:
Core Components, Part 2 211
image-feed/App.js
// ...
const styles = StyleSheet.create({
// ...
comments: {
flex: 1,
marginTop:
Platform.OS === 'ios' && platformVersion < 11
? Constants.statusBarHeight
: 0,
},
});
Like before, we need to handle iOS versions below 11 separately by adding a top margin. Modals
naturally sit below the status bar on Android, so we only need a top margin on iOS in this case.
After saving App.js, you’ll be able to open and close the Comments screen! Tap any "Comments" link
to open it, and tap the "Close" button in the NavigationBar to close it.
There should still be a warning about a missing onSubmitComment prop. Let’s add that next.
Adding new comments
Now that we can access our new Comments screen, we’ll want to be able to type new comments.
Let’s create a function property onSubmitComment on our App component for saving a new comment
into the commentsForItem object in our state. Since our commentsForItem object should be immutable
(it’s part of state), we’ll create a new object and copy over the existing keys and values using the
... object spread syntax. For our selectedItemId, we’ll either update the comments array within
commentsForItem, copying over existing comments with the ... array spread syntax, or we’ll create
a new array if this is the first comment.
image-feed/App.js
// ...
onSubmitComment = (text) => {
const { selectedItemId, commentsForItem } = this.state;
const comments = commentsForItem[selectedItemId] || [];
const updated = {
...commentsForItem,
[selectedItemId]: [...comments, text],
Core Components, Part 2 212
};
this.setState({ commentsForItem: updated });
};
// ...
Since we’re creating a new commentsForItem object, when we end up passing it into Feed, the Feed
will pass it to the FlatList as extraData, triggering a re-render (updating the "0 Comments" text).
Computed property names
When defining object literals, we can dynamically compute property names by putting array
brackets around the property name. For example:
const name = 'foo';
const obj = { [name]: 'bar' };
console.log(obj.foo); // => 'bar'
This is roughly equivalent to:
const name = 'foo';
const obj = {};
obj[name] = 'bar';
Computed property names are convenient in cases like the above example, where we want the object
literal to have property names based on dynamic values.
The last step for typing new comments is to pass the onSubmitComment function to the Comments
component when rendering:
image-feed/App.js
// ...
return (
<View style={styles.container}>
<Feed
style={styles.feed}
commentsForItem={commentsForItem}
onPressComments={this.openCommentScreen}
/>
Core Components, Part 2 213
<Modal
visible={showModal}
animationType="slide"
onRequestClose={this.closeCommentScreen}
>
<Comments
style={styles.comments}
comments={commentsForItem[selectedItemId] || []}
onClose={this.closeCommentScreen}
onSubmitComment={this.onSubmitComment}
/>
</Modal>
</View>
);
// ...
Save App.js, then go ahead and play around with the app for a bit! You should be able to tap the “0
Comments” text at the top right of each image to open up the Modal containing the Comments screen.
You should be able to type new comments and see them appear in the list of comments. When you
close the Modal, you should see the number of comments has increased for the image you chose.
Bonus: Persisting comments to device storage
You may have noticed that any comments you added will disappear if you reload the app. This is
because we don’t save them anywhere.
As an optional final step, we can persist the comments we write to the device via the AsyncStorage
API. AsyncStorage is a simple key-value store provided by React Native for storing small quan-
tities of string data (which we usually serialize as JSON). Like the name implies, saving and
reading from this store both happen asynchronously. We can call AsyncStorage.getItem(key) and
AsyncStorage.setItem(key, value) to store and retrieve a string value using a string key.
In App.js, we’ll first need to import AsyncStorage:
image-feed/App.js
import { AsyncStorage, Modal, Platform, StyleSheet, View } from 'react-native';
Then we’ll define an arbitrary key for persisting our comments object as JSON:
Core Components, Part 2 214
image-feed/App.js
const ASYNC_STORAGE_COMMENTS_KEY = 'ASYNC_STORAGE_COMMENTS_KEY';
Then we’ll update our componentDidMount and onSubmitComment to save and read from AsyncStorage,
respectively. Since we can only store string values using AsyncStorage, if we want to store a complex
object, we’ll have to serialize it to JSON first. To do this, we can call JSON.stringify before storing
values and JSON.parse after retreiving them.
We’ll load all comments into state when our App mounts:
image-feed/App.js
async componentDidMount() {
try {
const commentsForItem = await AsyncStorage.getItem(
ASYNC_STORAGE_COMMENTS_KEY,
);
this.setState({
commentsForItem: commentsForItem ? JSON.parse(commentsForItem) : {},
});
} catch (e) {
console.log('Failed to load comments');
}
}
Then we’ll update the stored comments anytime we add a new comment by modifying onSubmitComment:
// ...
onSubmitComment = text => {
const { selectedItemId, commentsForItem } = this.state;
const comments = commentsForItem[selectedItemId] || [];
const updated = {
...commentsForItem,
[selectedItemId]: [...comments, text],
};
this.setState({ commentsForItem: updated });
try {
AsyncStorage.setItem(ASYNC_STORAGE_COMMENTS_KEY, JSON.stringify(updated));
Core Components, Part 2 215
} catch (e) {
console.log('Failed to save comment', text, 'for', selectedItemId);
}
};
// ...
Note that getItem and setItem can both fail (e.g. when disk I/O fails), so we need to wrap any async
calls in try/catch.
That’s all we had to do to persist comments to disk! Now when you write comments and reload the
app, they’ll still be there. Give it a shot!
Wrapping up
Many of the built-in components we’ve covered in this chapter are highly generic and resusable:
View, Text, Image, ScrollView, and FlatList. The bulk of the UI in most apps will be written with
a combination of these components.
We covered a few other components which are for more specialized use cases, like ActivityIndicator,
TextInput, and Modal. There are many more components like this which we didn’t cover.
You don’t need to memorize every built-in React Native component in fact, there are some
components you’ll probably never need. The important thing is: you now have a strong foundation
in how React Native works, so you’ll be able to figure out how to use any built-in component just
by reading the docs.
UI components are a huge part of what React Native has to offer. However, most apps need more than
just UI components. There’s another big part of React Native which we touched on in this chapter:
imperative APIs. These are APIs (like AsyncStorage) which we can call from the component lifecycle
to fetch data, access the camera roll, query our geolocation, etc. In the next chapter, we’ll explore
some of the most common React Native APIs.
Core APIs, Part 1
So far we’ve primarily used React components to interact with the underlying native APIs we’ve
used components like View, Text, and Image to create native UI elements on the screen.
React provides a simple, consistent interface for APIs which create visual components. Some APIs
don’t create UI components though: for example, accessing the Camera Roll, or querying the current
network connectivity of the device.
React Native also comes with APIs for interacting with these non-visual native APIs. In contrast
with components, these APIs are generally imperative functions: we must call them explicitly at
the right time, rather than returning something from a component’s render function and letting
React call them later. React Native simply provides us a JavaScript wrapper, often cross-platform,
for controlling the underlying native APIs.
Building a messaging app
In this chapter, we’ll build the start of a messaging app (similar to iMessage) that gives us a tour of
some of the most common core APIs. Our app will let us send text, send photos from the camera
roll, and share our location. It will let us know when we are disconnected from the network. It will
handle keyboard interactions and the back button on Android.
To try the completed app:
On Android, you can scan the following QR code from within the Expo app:
On iOS, you can navigate to the messaging/ directory within our sample code folder and build
the app. You can either preview it using the iOS simulator or send the link of the project URL
to your device as we mentioned in the first chapter.
Core APIs, Part 1 217
We can send text messages, images, and maps:
We can choose images from our device camera roll:
Core APIs, Part 1 218
And we can view images fullscreen:
Core APIs, Part 1 219
We’ll use the following APIs:
Alert - Displays modal dialog windows for simple user input
BackHandler - Controls the back button on Android
CameraRoll - Returns images and videos stored on the device
Dimensions - Returns the dimensions of the screen
Geolocation - Returns the location of the device, and emits events when the location changes
Keyboard - Emits events when the keyboard appears or disappears
NetInfo - Returns network connectivity information, and emits events when the connectivity
changes
PixelRatio - Translates from density-independent pixels to density-dependent pixels (more
detail on the later)
StatusBar - Controls the visibility and color of the status bar
We’ll just be focusing on the UI, so we won’t actually send messages, but we could connect the UI
we build to a backend if we wanted to use it in a production app.
Initializing the project
Just as we did in the previous chapters, let’s create a new app with the following command:
Core APIs, Part 1 220
$ create-react-native-app messaging --scripts-version 1.14.0
Once this finishes, navigate into the messaging directory.
In this chapter we’ll create the utils directory ourselves, so there’s no need to copy over the sample
code.
The app
Let’s start by setting up the skeleton of the app. We’ll do this in App.js. After that, we’ll build out
the different parts of the screen, one component at a time. We’ll tackle keyboard handling last, since
that’s the most difficult and intricate.
We’ll follow the same general process as in the previous chapters: we’ll start by breaking down the
screen into components, building a hardcoded version, adding state, and so on.
The app’s skeleton
If we look at the app from top to bottom, these are the main sections of the UI:
Core APIs, Part 1 221
Status - The device generally renders a status bar, the horizontal strip at the top of the
screen that shows time, battery life, etc but in this case, we’ll augment it to show network
connectivity more prominently. We’ll create our own component, Status, which renders
beneath the device’s status bar.
MessageList - This is where we’ll render text messages, images, and maps.
Toolbar - This is where the user can switch between sending text, images, or location, and
where the input field for typing messages lives.
Input Method Editor (IME) - This is where we can render a custom input method, i.e. sending
images. We’ll build an image picker component, ImageGrid, and use it here. Note that the
keyboard is rendered natively by the operating system, so we will trigger the keyboard to
appear and disappear at the right times, but we won’t render it ourselves.
In this chapter we’ll be building top-down. We’ll start by representing the message list, the toolbar,
and the IME with a placeholder View. By starting with a rough layout, we can then create components
for each section, putting each one in its respective View. Each section will be made up of a few
different components.
Core APIs, Part 1 222
Open App.js and add the following skeleton:
messaging/App.js
import { StyleSheet, View } from 'react-native';
import React from 'react';
export default class App extends React.Component {
renderMessageList() {
return (
<View style={styles.content}></View>
);
}
renderInputMethodEditor() {
return (
<View style={styles.inputMethodEditor}></View>
);
}
renderToolbar() {
return (
<View style={styles.toolbar}></View>
);
}
render() {
return (
<View style={styles.container}>
{this.renderMessageList()}
{this.renderToolbar()}
{this.renderInputMethodEditor()}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
content: {
flex: 1,
Core APIs, Part 1 223
backgroundColor: 'white',
},
inputMethodEditor: {
flex: 1,
backgroundColor: 'white',
},
toolbar: {
borderTopWidth: 1,
borderTopColor: 'rgba(0,0,0,0.04)',
backgroundColor: 'white',
},
});
When you save App.js, the app should reload on your device and you’ll see the following:
Awesome, a blank screen with a small gray line through the middle! Now we can start building out
the different sections of the screen. The App component will orchestrate how data is populated, and
when to hide or show the various input methods but first, we need to start creating the different
components in the UI.
Now’s a good time to create a new directory, components, within our main messaging directory.
We’ll put the UI components we build in the components directory.
Core APIs, Part 1 224
Network connectivity indicator
Since we’re building a messaging app, network connectivity is relevant at all times. Let’s let the user
know when they’ve lost connectivity by turning the status bar red and displaying a short message.
StatusBar
Many apps display the default status bar, but sometimes we want to customize the style, e.g. turning
the background red.
The status bar works a little differently on iOS and Android. On iOS the status bar background is
always transparent, so we can render content behind the status bar text. On Android, we can set
the status bar background to transparent, or to a specific solid color. If we use a transparent status
bar, we can render content behind it just like on iOS unlike on iOS, by default the status bar text
is white and there’s typically a semi-transparent black background. If we choose a solid color status
bar, our app’s content renders below the status bar, and the height of our UI will be a little smaller.
In our app, we’ll use a solid color status bar, since this will let us customize the color.
To use a solid color status bar, we need to open up app.json and add the following to the expo object
(although you can skip this if you’re not using an Android):
Core APIs, Part 1 225
messaging/app.json
"expo": {
// ...
"androidStatusBar": {
"barStyle": "dark-content",
"backgroundColor": "#FFFFFF"
}
}
Let’s restart the packager with npm run start to make sure this change takes effect.
If we had used react-native init instead of create-react-native-app, we wouldn’t
need to do this. Expo handles the status bar specially. You can check out the guide on
configuring the status bar
61
for more detail.
On both platforms, we can set the status bar text color by using the built-in StatusBar component
and passing a barStyle of either light-content (white text) or dark-content (black text).
There are two different ways we can use StatusBar: imperatively and as a component. In this
example we’ll use the component approach.
Create a new file Status.js in the components directory now.
Status styles
Let’s first start with the background styles. We need to create a View that sits behind the text of the
status bar on iOS, rendering the background color of the status bar is our responsibility, since the
operating system only renders the status bar text.
We’ll have two visual states: one where the user is connected to the network, and one where the
user is disconnected. We’ll set the color for each state in render, so let’s start with the base style for
the status bar:
61
https://docs.expo.io/versions/latest/guides/configuring-statusbar.html
Core APIs, Part 1 226
messaging/components/Status.js
import { Constants } from 'expo';
import { StyleSheet } from 'react-native';
// ...
const statusHeight =
(Platform.OS === 'ios' ? Constants.statusBarHeight : 0);
const styles = StyleSheet.create({
status: {
zIndex: 1,
height: statusHeight,
},
// ...
});
The base style status will give the View its height. The View will have the same height regardless of
whether this component is in the connected or disconnected state. We use a zIndex of 1 to indicate
that this View should be drawn on top of other content this will be relevant later, since we’re going
to render a ScrollView beneath it.
Depending on the component’s state, we’ll then pass a style object containing a background color
(in addition to passing the status style).
We’ll store the network connectivity status in component state as state.info. Network connectiv-
ity status can have several different states, but let’s assume for now that if the state.info is "none"
then we’re disconnected, and anything else means we’re connected.
Let’s try rendering this background View.
messaging/components/Status.js
import { Constants } from 'expo';
import { NetInfo, Platform, StatusBar, StyleSheet, Text, View } from 'react-native';
import React from 'react';
export default class Status extends React.Component {
state = {
info: null,
};
// ...
render() {
Core APIs, Part 1 227
const { info } = this.state;
const isConnected = info !== 'none';
const backgroundColor = isConnected ? 'white' : 'red';
if (Platform.OS === 'ios') {
return <View style={[styles.status, { backgroundColor }]}></View>;
}
return null; // Temporary!
}
}
// ...
Notice how we use an array for the View to apply two styles: the status style, and then a style object
containing a different background color depending on whether we’re connected to the network or
not.
Let’s save Status.js and import it from App.js so we can see what we have so far.
We can now go ahead and render our new Status component from App:
messaging/App.js
// ...
import Status from './components/Status';
export default class App extends React.Component {
// ...
render() {
return (
<View style={styles.container}>
<Status />
{this.renderMessageList()}
{this.renderToolbar()}
{this.renderInputMethodEditor()}
</View>
);
}
// ...
Core APIs, Part 1 228
}
// ...
We shouldn’t see anything yet… but to verify that everything is working, you can temporarily set
info: 'none' in the state of Status. This will show a red background behind the status bar text.
Doing this, we should see:
Using StatusBar
The black text on the red background doesn’t look very good. This is where the StatusBar component
comes in. Let’s import it from react-native and render it within our View.
Core APIs, Part 1 229
messaging/components/Status.js
import { Constants } from 'expo';
import { StatusBar, StyleSheet, View } from 'react-native';
import React from 'react';
export default class Status extends React.Component {
state = {
info: null,
};
// ...
render() {
const { info } = this.state;
const isConnected = info !== 'none';
const backgroundColor = isConnected ? 'white' : 'red';
const statusBar = (
<StatusBar
backgroundColor={backgroundColor}
barStyle={isConnected ? 'dark-content' : 'light-content'}
animated={false}
/>
);
if (Platform.OS === 'ios') {
return <View style={[styles.status, { backgroundColor }]}>{statusBar}</View>;
}
return null; // Temporary!
}
}
Here we set barStyle to dark-content if we’re connected (black text on our white background) and
light-content if we’re disconnected (white text on our red background). We set backgroundColor
to set the correct background color on Android. We also set animated to false since we’re not
animating the background color on iOS, animating the text color won’t look very good.
Note that the StatusBar component doesn’t actually render the status bar text. We use this
component to configure the status bar. We can render the StatusBar component anywhere in the
component hierarchy of our app to configure it, since the status bar is configured globally.
Core APIs, Part 1 230
We can even render StatusBar in multiple different components, e.g. we could render it from App.js
in addition to Status.js. If we do this, the props we set as configuration are merged in the order the
components mount. In practice it can be a bit hard to follow the mount order, so it may be easier to
use the imperative API if you find yourself with many StatusBar components (more on this later).
Message bubble
Since the red status bar alone doesn’t indicate anything about network connectivity, let’s also add a
short message in a floating bubble at the top of the screen.
If we’re not connected to the network, we’ll render a few more components. On Android, since we
don’t need to render the background behind the status bar, we can return just the message bubble
components.
Core APIs, Part 1 231
messaging/components/Status.js
import { Constants } from 'expo';
import { NetInfo, StatusBar, StyleSheet, Text, View } from 'react-native';
import React from 'react';
export default class Status extends React.Component {
// ...
render() {
const { info } = this.state;
const isConnected = info !== 'none';
const backgroundColor = isConnected ? 'white' : 'red';
const statusBar = (
<StatusBar
backgroundColor={backgroundColor}
barStyle={isConnected ? 'dark-content' : 'light-content'}
animated={false}
/>
);
const messageContainer = (
<View style={styles.messageContainer} pointerEvents={'none'}>
{statusBar}
{!isConnected && (
<View style={styles.bubble}>
<Text style={styles.text}>No network connection</Text>
</View>
)}
</View>
);
if (Platform.OS === 'ios') {
return <View style={[styles.status, { backgroundColor }]}>{messageContainer}</\
View>;
}
return messageContainer;
}
}
const styles = StyleSheet.create({
Core APIs, Part 1 232
// ...
messageContainer: {
zIndex: 1,
position: 'absolute',
top: statusHeight + 20,
right: 0,
left: 0,
height: 80,
alignItems: 'center',
},
bubble: {
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 20,
backgroundColor: 'red',
},
text: {
color: 'white',
},
});
A future update to this chapter will fix iPhone X style issues.
Here we use absolute position to precisely position the message bubble on top of the rest of the con-
tent we’ll render, without pushing our other content out of the way. We use pointerEvent={'none'}
so that this component doesn’t prevent us from tapping the ScrollView we’ll render it. The
pointerEvents prop allows us to control whether an component can respond to touch interactions,
or whether they pass through to the components behind it.
Save Status.js and you should see the following.
Core APIs, Part 1 233
Our connectivity indicator UI is looking good! Now it’s time to hook it up to the device’s real network
connectivity state.
NetInfo
We have info in state, and we have logic to switch between showing a connected and disconnected
UI in our render method. Now we need to update this state whenever network connectivity changes.
We can do this using the NetInfo APIs.
The NetInfo APIs are a good example of React Native core APIs: these provide a uniform interface
to the lower level native APIs on iOS and Android. React Native is essentially providing JavaScript
bindings and smoothing out platform differences for us.
We can call NetInfo.getConnectionInfo() to get the network connectivity status. NetInfo.getConnectionInfo()
returns a promise which resolves to a string. If the device is connected, the string value will be 'wifi'
or 'cellular' If the device isn’t connected, the promise will still resolve, but with the value 'none'.
If we wanted to update our UI when the network connection changes, we could continuously poll
NetInfo.getConnectionInfo() to get the network status but this would be inefficient. Instead, we
can add an event listener to NetInfo. NetInfo provides the method addEventListener, which we
can call with a callback function, which it will invoke each time the network status changes.
Here’s an example of using NetInfo.addEventListener:
Core APIs, Part 1 234
const subscription = NetInfo.addEventListener('connectionChange', (status) => {
console.log('Network status changed', status)
});
This example would log a new status each time the network connectivity changes. We can call
subscription.remove() when we want to stop listening for changes most of the time, we’ll do
this when our component unmounts.
For our app, we’ll use both NetInfo.getConnectionInfo and NetInfo.addEventListener. First we’ll
call NetInfo.getConnectionInfo when the Status component mounts to get the initial network
connectivity. Then we’ll use NetInfo.addEventListener to update our UI when a change occurs.
Let’s add the following lines to our Status component in Status.js:
1 // ...
2
3 async componentWillMount() {
4 this.subscription = NetInfo.addEventListener('connectionChange', this.handleChange\
5 );
6
7 const info = await NetInfo.getConnectionInfo();
8
9 this.setState({ info });
10 }
11
12 componentWillUnmount() {
13 this.subscription.remove();
14 }
15
16 handleChange = (info) => {
17 this.setState({ info });
18 };
19
20 // ...
Now we receive both the initial status and handle connectivity changes.
Note that we declared componentWillMount as an async method, so that we can use await when
calling NetInfo.getConnectionInfo(). Most of the React lifecycle methods can be declared with
async, since React doesn’t use the return value from these.
To test changes in network connectivity without setting the device to airplane mode,
we can add: setTimeout(() => this.handleChange('none'), 3000); to the end of
componentWillMount. This way we can observe the transition from our initial state
(probably 'wifi') to the disconnected state.
Core APIs, Part 1 235
For reference, if we wanted to use the imperative approach to changing the status bar style, we
would write our handleChange as:
handleChange = (info) => {
this.setState({ info });
StatusBar.setBarStyle(info === 'none' ? 'light-content' : 'dark-content');
};
We would then remove the <StatusBar ... /> component from our render function. The
StatusBar component is a little unusual because it doesn’t actually render anything. Under the
hood, the StatusBar component just calls StatusBar.setBarStyle at the appropriate times. Calling
the imperative APIs directly can be simpler than figuring out how and where to render StatusBar
components in a complex app.
Wrapping up StatusBar and NetInfo
We’re finished with the status bar and network connectivity indicator! We’ve just written a cross-
platform UI that works on both iOS and Android, with only a little bit of platform-specific code.
For future improvements, we could consider animating the message bubble as it appears and
disappears, and animating the status bar as it changes colors. We’ll cover this kind of animation
in more depth in a later chapter. For now, let’s move on to the message list.
The message list
Let’s create the message list. The message list will display a vertically scrolling list of text messages,
image messages, and location messages. We should be able to tap the messages to potentially trigger
other actions (e.g. view the image fullscreen).
Core APIs, Part 1 236
We’ll use the
FlatList
component we learned about in the previous chapter to handle rendering the
list. In order to do that, we should first decide how we’ll store our message objects.
MessageUtils
Let’s first write a few utility functions for creating message objects so that we keep this logic separate
from our rendering logic.
Create a new directory called utils in the messaging directory. Within utils, create a new file
called MessageUtils.js.
Within MessageUtils.js, let’s first define the shape of each message using PropTypes.shape. All of
the messages we render will have a type and an id, and then some messages will have either a text,
uri, or coordinate value, depending on the type.
Add the following to MessageUtils.js:
Core APIs, Part 1 237
messaging/utils/MessageUtils.js
import PropTypes from 'prop-types';
export const MessageShape = PropTypes.shape({
id: PropTypes.number.isRequired,
type: PropTypes.oneOf(['text', 'image', 'location']),
text: PropTypes.string,
uri: PropTypes.string,
coordinate: PropTypes.shape({
latitude: PropTypes.number.isRequired,
longitude: PropTypes.number.isRequired,
}),
});
By using this shape in the propTypes of a component, React will automatically warn us if we
accidentally pass invalid message data. Declaring our data models this way is also great for
documentation purposes: if another developer reads our component, they’ll know exactly what the
input data should look like, without having to sprinkle console.log throughout the app and actually
run it.
Declaring our data models this way is optional. For a model that will likely be used in many
places throughout the app, it’s probably worthwhile to spend the extra effort. It isn’t as
valuable for a model used within a single component, or a model that you’re still iterating
on during development. If you decide to use a strongly-typed variant of JavaScript, i.e. Flow
or TypeScript, you’ll likely declare your types elsewhere and won’t need to also declare
PropTypes.
Next, let’s write a few utility functions for creating the different kinds of messages:
messaging/utils/MessageUtils.js
let messageId = 0;
function getNextId() {
messageId += 1;
return messageId;
}
export function createTextMessage(text) {
return {
type: 'text',
id: getNextId(),
text,
Core APIs, Part 1 238
};
}
export function createImageMessage(uri) {
return {
type: 'image',
id: getNextId(),
uri,
};
}
export function createLocationMessage(coordinate) {
return {
type: 'location',
id: getNextId(),
coordinate,
};
}
We created a utility getNextId() for getting a unique message id. It’s important that we ensure
uniqueness for each id, since we’ll be using the id as the key when rendering these messages in a
list.
We would likely want to use a more sophisticated id, such as a UUID, if we were actually
connecting with a backend. Incrementing a number works for our purposes, but once
messages are persisted or coming from multiple devices, there would be id collisions.
By exporting createTextMessage, createImageMessage, and createLocationMessage, we can now
easily create new messages of each type from elsewhere in our app. We’ll use these messages to
populate the FlatList.
MessageList
Our MessageList component will render an array of the message objects we defined in MessageUtils.
We can determine how to render each message based on its type.
Let’s start by determining the propTypes for this component. Here’s a good opportunity to use the
MessageShape we just defined. We’ll also want to notify the parent component whenever a message
in the list is pressed. We can do this using an onPressMessage function prop.
Create a new file, MessageList.js, in our components directory. Add the following to it:
Core APIs, Part 1 239
messaging/components/MessageList.js
import React from 'react';
import PropTypes from 'prop-types';
import { MessageShape } from '../utils/MessageUtils';
export default class MessageList extends React.Component {
static propTypes = {
messages: PropTypes.arrayOf(MessageShape).isRequired,
onPressMessage: PropTypes.func,
};
static defaultProps = {
onPressMessage: () => {},
};
// ...
}
By using our MessageShape, React will warn us if we’re passed malformed data.
Now let’s render our messages into a FlatList. Just as in the previous chapter, We need to use a
keyExtractor to tell the FlatList how to find the unique id of our message objects.
Let’s update MessageList.js to render a FlatList:
messaging/components/MessageList.js
import { FlatList, StyleSheet } from 'react-native';
// ...
const keyExtractor = item => item.id.toString();
export default class MessageList extends React.Component {
// ...
renderMessageItem = ({ item }) => {
// ...
};
render() {
const { messages } = this.props;
Core APIs, Part 1 240
return (
<FlatList
style={styles.container}
inverted
data={messages}
renderItem={this.renderMessageItem}
keyExtractor={keyExtractor}
keyboardShouldPersistTaps={'handled'}
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
overflow: 'visible', // Prevents clipping on resize!
},
});
We looked at the data, renderItem, and keyExtractor props in the previous chapter. There are a few
new props here that are worth looking at in more detail.
inverted
In a messaging app, we typically want new messages to appear at the bottom of the list. To
accomplish this, we’ve added the inverted prop to our FlatList.
This “new-messages-at-the-bottom” behavior is difficult to achieve without using inverted.
If we didn’t use inverted, every time a new message is added, we would have to scroll to the
bottom of the list by adding a ref to the list and calling the scrollToEnd method. While it
may sound relatively simple, it quickly gets complicated when we start adding asynchronous
animations, e.g. in response to the keyboard appearing. Since ScrollView doesn’t support
inverted, we almost always want to use a FlatList for this.
Behind-the-scenes, our FlatList is vertically inverted using a transform style, and then
each row within the list is also vertically inverted. Since rows are doubly inverted, they
appear right-side-up. Pretty clever!
keyboardShouldPersistTaps
We use the keyboardShouldPersistTaps prop to configure what happens when we tap the FlatList.
This prop has three possible options:
Core APIs, Part 1 241
never - Tapping the list will dismiss the keyboard and blur any focused elements. This is the
default behavior.
always - Tapping the list will have no effect on the keyboard or focus.
handled - Tapping the list will dismiss the keyboard, unless the tap is handled by a child
element first (e.g. tapping a message within the list). We want handled, so that we enable
tapping messages without dismissing the keyboard.
We add overflow: 'visible' to the style of the FlatList to prevent content from getting clipped
during animations. When an animation causes the list to resize to a smaller size, the content within it
will be clipped to the smaller size instantly, while the list itself resizes gradually. If we don’t include
this line, some content will get clipped at the start of the animation that should actually be clipped
at the end. It’ll be easier to understand why this is necessary after we’re a bit further along. You can
comment out this property and observe the difference in behavior as the keyboard appears.
Rendering messages
We’ve successfully set up a scrolling list, so now we can populate it with messages. As a reminder,
this is what we’re aiming to build:
Let’s start with the styles. Conceptually, each message is a row in the list, so let’s call our top-level
message style messageRow and give it flexDirection: 'row'. We want to align messages to the right
Core APIs, Part 1 242
using justifyContent: 'flex-end', but leave a little space on the left with marginLeft: 60 in case
our message gets long. Text messages should appear in blue bubbles: this is what messageBubble is
for.
messaging/components/MessageList.js
const styles = StyleSheet.create({
container: {
flex: 1,
overflow: 'visible', // Prevents clipping on resize!
},
messageRow: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginBottom: 4,
marginRight: 10,
marginLeft: 60,
},
messageBubble: {
paddingVertical: 5,
paddingHorizontal: 10,
backgroundColor: 'rgb(16,135,255)',
borderRadius: 20,
},
text: {
fontSize: 18,
color: 'white',
},
image: {
width: 150,
height: 150,
borderRadius: 10,
},
map: {
width: 250,
height: 250,
borderRadius: 10,
},
});
Feel free to experiment with other styles. There are a lot of ways to customize a messaging
app to give it a unique look.
Core APIs, Part 1 243
Let’s move on to renderMessageItem and begin using the styles we just created. We can start by
updating our imports to include all of the components we’ll render from MessageList:
import { FlatList, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-nat\
ive';
import { MapView } from 'expo';
Here we’ll use MapView for the first time. You’ll notice we import this from Expo, rather than from
React Native. MapView comes from the 3rd party module react-native-maps, which Expo includes
by default. If we had created our app via react-native-cli rather than create-react-native-app,
we would have to remember to install and link react-native-maps.
If you’re running an Android emulator, you’ll need the Google Play Services installed to
actually see a MapView (otherwise you’ll see a placeholder label). You can create an emulator
from Android Studio with this, but it can be difficult to connect it to Expo. If you don’t know
how to do this already, we recommend testing on a real Android device if possible.
React Native used to include a built-in MapView, but this has been removed in favor of
react-native-maps. The react-native-maps module quickly became the de-facto standard
for using maps in React Native, obsoleting the original built-in version.
Then let’s add the following:
messaging/components/MessageList.js
// ...
renderMessageItem = ({ item }) => {
const { onPressMessage } = this.props;
return (
<View key={item.id} style={styles.messageRow}>
<TouchableOpacity onPress={() => onPressMessage(item)}>
{this.renderMessageBody(item)}
</TouchableOpacity>
</View>
);
};
renderMessageBody = ({ type, text, uri, coordinate }) => {
// ...
}
Core APIs, Part 1 244
// ...
Just as in the previous chapters, we use the id of the item as the key of the top-level element we
return, so React can keep track of existing items. Without this, React would have to re-render all
items when we add more, since it wouldn’t know which items are old and which are new.
We want each message to be tappable, so we wrap the message body in a TouchableOpacity, and
call onPressItem with the item object when tapped.
Finally, we call this.renderMessageBody with the item (our message), where we’ll render the body
of each message. We’ll switch on the type of the message to decide what to render.
messaging/components/MessageList.js
export default class MessageList extends React.Component {
// ...
renderMessageBody = ({ type, text, uri, coordinate }) => {
switch (type) {
case 'text':
return (
<View style={styles.messageBubble}>
<Text style={styles.text}>{text}</Text>
</View>
);
case 'image':
return <Image style={styles.image} source={{ uri }} />;
case 'location':
return (
<MapView
style={styles.map}
initialRegion={{
...coordinate,
latitudeDelta: 0.08,
longitudeDelta: 0.04,
}}
>
<MapView.Marker coordinate={coordinate} />
</MapView>
);
default:
return null;
}
};
Core APIs, Part 1 245
// ...
}
Each type of message, text, image, and location has a different kind of UI. Most of the components
we used for text and image should look pretty familiar.
For location messages, we use a MapView. The MapView API is fairly advanced, allowing custom
drawing and animations on top of maps. We’ll use the initialRegion to supply a bounding box
to display on the map, and we’ll create a MapView.Marker to drop a pin at the coordinate in the
message. You can read more about the MapView API in the official docs for react-native-maps
62
.
We now have a scrollable, tappable list of messages that supports different kinds of content. Let’s
test it out.
Adding MessageList to App
To test our new MessageList component, we can render it from within the renderMessageList
method of App.js.
Heading back to App.js now, we’ll need to import our new MessageList component and our utility
functions for creating messages:
messaging/App.js
// ...
import MessageList from './components/MessageList';
import { createImageMessage, createLocationMessage, createTextMessage } from './util\
s/MessageUtils';
//...
We can use these utility functions to create a few sample messages in the initial state of our app:
62
https://github.com/airbnb/react-native-maps
Core APIs, Part 1 246
messaging/App.js
// ...
state = {
messages: [
createImageMessage('https://unsplash.it/300/300'),
createTextMessage('World'),
createTextMessage('Hello'),
createLocationMessage({
latitude: 37.78825,
longitude: -122.4324,
}),
],
};
handlePressMessage = () => {}
renderMessageList() {
const { messages } = this.state;
return (
<View style={styles.content}>
<MessageList messages={messages} onPressMessage={this.handlePressMessage} />
</View>
);
}
// ...
We’ve now hooked up the hardcoded message data with our MessageList component. We also
added a placeholder handlePressMessage for handling tapping messages. When you save App.js, if
everything is working correctly, here’s what you should see:
Core APIs, Part 1 247
We’re successfully rendering our message list! We can display messages of different types and our
new messages appear at the bottom, just like we’d expect from a messaging app.
At this point, we have the prop onPressMessages set to the empty function handlePressMessage.
Let’s hook up a few different actions to onPressMessages.
Alert
The first action we’ll add is to text messages. We’ll add a “delete” feature: when the user taps a
text message, we’ll give the user the option to delete that message. We’ll present a dialog with two
choices: delete and cancel.
We can use the Alert.alert API to present the user with a native dialog window for making this
choice.
Core APIs, Part 1 248
Alert dialogs are commonly used for asking simple “yes or no” questions. The text and quantity of
buttons are configurable.
Alert dialogs can also be used for debugging. Sometimes it can be easier to pop open an alert
dialog than tracking down a console.log message.
The full method signature is Alert.alert(title, message?, buttons?, options?, type?):
title - A string, shown in a large bold font, at the top of the dialog
message - A string, typically longer, shown in a normal weight font below the title
buttons - An array of objects containing text (a string), onPress (a callback function), and
optionally a style on iOS (styles can be one of default, cancel, or destructive).
options - An object for controlling the dialog dismissal behavior on Android. Tapping outside
the dialog will normally exit the dialog. This can be prevented by setting { cancelable: false
} or handled specially with { onDismiss: () => {} }.
type - Allows text entry on iOS using one of the following options: default, plain-text,
secure-text, or login-password.
Let’s trigger an alert from App.js. First we’ll need to import Alert:
Core APIs, Part 1 249
messaging/App.js
import {
Alert,
// ...
} from 'react-native';
Then we can add the following to our handlePressMessage method:
messaging/App.js
// ...
handlePressMessage = ({ id, type }) => {
switch (type) {
case 'text':
Alert.alert(
'Delete message?',
'Are you sure you want to permanently delete this message?',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Delete',
style: 'destructive',
onPress: () => {
const { messages } = this.state;
this.setState({ messages: messages.filter(message => message.id !== id\
) });
},
},
],
);
break;
default:
break;
}
};
// ...
After adding these lines, save App.js. When we tap a text message, we should see something like
this:
Core APIs, Part 1 250
Pressing “Delete” will remove the message from the list. We do this by filtering the list of messages
and removing the message with the id that we tapped. The removal currently isn’t animated, but it
will be when we’re finished with this app!
It’s fairly easy to call Alert incorrectly. If you’re coming from the web, you might attempt
to call Alert() rather than Alert.alert. You also might try to call Alert.alert with
parameters that are numbers instead of strings, e.g. our message id. Both of these will crash
the app with confusing error messages. It’s also possible to get into a corrupted state, where
you’ll have to restart the app before Alert.alert will function properly again.
Deleting text messages is useful, but what should we do when the user taps other kinds of messages?
For images, let’s show the image fullscreen.
Fullscreen image
When the user presses an image, we’ll show it fullscreen.
Core APIs, Part 1 251
Transitioning to fullscreen might be accomplished using a navigation library (which we’ll cover in
a later chapter), but we can also do it manually. If we do, we’ll want the Android back button to
dismiss the fullscreen image we can use the BackHandler API to accomplish this. Let’s also dismiss
the image when it’s pressed again, so that we’re not trapped in a fullscreen image state on iOS.
In App.js, we’ll use state to keep track of which image was pressed. Let’s add the following for
state tracking:
messaging/App.js
// ...
state = {
// ...
fullscreenImageId: null,
};
dismissFullscreenImage = () => {
this.setState({ fullscreenImageId: null });
};
// ...
Core APIs, Part 1 252
We’ve initialized fullscreenImageId to null to indicate that we don’t want to show any image.
Then, when a message is pressed, we’ll set it to the id of the message object. We can update
handlePressMessage with the following:
messaging/App.js
// ...
handlePressMessage = ({ id, type }) => {
switch (type) {
case 'text':
// ...
case 'image':
this.setState({ fullscreenImageId: id });
break;
default:
break;
}
};
// ...
Now we need to render the fullscreen image. We’ll use a TouchableHighlight for the black overlay
background so we can tap it to dismiss the image. Within that, we’ll use an Image. We’ll use the
fullscreenImageId to look up which image we need to display as the uri of the Image component.
First, let’s import Image and TouchableHighlight:
messaging/App.js
import {
// ...
Image,
TouchableHighlight,
} from 'react-native';
Then we can set up the styles:
Core APIs, Part 1 253
messaging/App.js
const styles = StyleSheet.create({
// ...
fullscreenOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'black',
zIndex: 2,
},
fullscreenImage: {
flex: 1,
resizeMode: 'contain',
},
});
We’ll use the built-in StyleSheet.absoluteFillObject so that our overlay background is fullscreen,
and then we’ll add zIndex: 2 so that it renders on top of the rest of our UI. Our image should fill
the overlay, so we use flex: 1.
Next, let’s create a helper method for rendering the fullscreen image called renderFullscreenImage.
We can also use this method to determine if we need to show an image. We’ll call this from render.
Add the following:
messaging/App.js
// ...
renderFullscreenImage = () => {
const { messages, fullscreenImageId } = this.state;
if (!fullscreenImageId) return null;
const image = messages.find(message => message.id === fullscreenImageId);
if (!image) return null;
const { uri } = image;
return (
<TouchableHighlight style={styles.fullscreenOverlay} onPress={this.dismissFullsc\
reenImage}>
<Image style={styles.fullscreenImage} source={{ uri }} />
</TouchableHighlight>
);
Core APIs, Part 1 254
};
// ...
render() {
return (
<View style={styles.container}>
<Status />
{this.renderMessageList()}
{this.renderToolbar()}
{this.renderInputMethodEditor()}
{this.renderFullscreenImage()}
</View>
);
}
Save App.js. Now when we tap an image, we should see it fullscreen, and we can tap it again to
dismiss it:
On Android though, we’ll also want the device’s back button to dismiss the image. We do this using
the BackHandler API.
Core APIs, Part 1 255
BackHandler
If you’re not using an Android, you can skip this section!
Like with NetInfo, we use the event listener pattern to handle back button press events:
`BackHandler.addEventListener('hardwareBackPress', handlerFunction);`
We can use this to get notified every time the user presses the back button on an Android device.
We’ll have our handlerFunction hide our fullscreen image.
We can return true from our handlerFunction to indicate that we’ve handled the back button. By
returning false, we indicate that we didn’t handle the event. Therefore, if any other functions have
been registered, the next one registered should be called. These functions are called in the reverse of
the order they were registered the last handler registered will be called first. If no handler returns
true, then the back button will exit to the home screen (the default back button behavior).
First, import the BackHandler API:
messaging/App.js
import {
// ...
BackHandler,
} from 'react-native';
Then we’ll use componentWillMount and componentWillUnmount to listen to back button presses:
messaging/App.js
// ...
componentWillMount() {
this.subscription = BackHandler.addEventListener('hardwareBackPress', () => {
const { fullscreenImageId } = this.state;
if (fullscreenImageId) {
this.dismissFullscreenImage();
return true;
}
return false;
});
Core APIs, Part 1 256
}
componentWillUnmount() {
this.subscription.remove();
}
// ...
If state.fullscreenImageId exists, then we’re currently showing a fullscreen image, so we’ll want
the back button to dismiss it. We return true to indicate that we shouldn’t exit the app. If we’re not
showing a fullscreen image, we return false. Because no other handlers should be registered, this
will allow for the default back button behavior (exiting the app).
Our message list is working pretty well now! We’ve used two core APIs, Alert and BackHandler,
to create cross-platform interactions for deleting and enlarging messages. Now that we’ve finished
with the message list, let’s move on to the next section of the UI and create the toolbar.
Toolbar
The toolbar will sit above the keyboard and contain an input field for typing messages, along with
buttons for switching to an image picker and sending a location.
Core APIs, Part 1 257
Building the Toolbar
The toolbar is similar to the CommentInput from the Core Components chapter: it maintains the
state of a TextInput field internally and uses an onSubmit function prop to tell the parent when the
message is ready to send. We’ll need a little more control over the focus state of the TextInput this
time, so we’ll use an isFocused prop to control the focus state and an onChangeFocus function prop
to tell the parent when the state should changes.
Let’s create a new file Toolbar.js in the components directory, and render the top level View which
will contain all the elements in the toolbar. We’ll add propTypes for the focus state and the various
functions which the parent component can pass in:
Core APIs, Part 1 258
messaging/components/Toolbar.js
import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
export default class Toolbar extends React.Component {
static propTypes = {
isFocused: PropTypes.bool.isRequired,
onChangeFocus: PropTypes.func,
onSubmit: PropTypes.func,
onPressCamera: PropTypes.func,
onPressLocation: PropTypes.func,
};
static defaultProps = {
onChangeFocus: () => {},
onSubmit: () => {},
onPressCamera: () => {},
onPressLocation: () => {},
};
render() {
return (
<View style={styles.toolbar}>
{/* ... */}
</View>
);
}
}
const styles = StyleSheet.create({
toolbar: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
paddingHorizontal: 10,
paddingLeft: 16,
backgroundColor: 'white',
},
// ...
});
Core APIs, Part 1 259
Let’s add a camera button and a location button and use them to call the onPressCamera and
onPressLocation props.
1 // ...
2
3 const ToolbarButton = ({ title, onPress }) => (
4 <TouchableOpacity onPress={onPress}>
5 <Text style={styles.button}>{title}</Text>
6 </TouchableOpacity>
7 );
8
9 ToolbarButton.propTypes = {
10 title: PropTypes.string.isRequired,
11 onPress: PropTypes.func.isRequired,
12 };
13
14 export default class Toolbar extends React.Component {
15 // ...
16
17 render() {
18 const { onPressCamera, onPressLocation } = this.props;
19
20 return (
21 <View style={styles.toolbar}>
22 {/* Use emojis for icons instead! */}
23 <ToolbarButton title={'C'} onPress={onPressCamera} />
24 <ToolbarButton title={'L'} onPress={onPressLocation} />
25 {/* ... */}
26 </View>
27 );
28 }
29 }
30
31 const styles = StyleSheet.create({
32 // ...
33 button: {
34 top: -2,
35 marginRight: 12,
36 fontSize: 20,
37 color: 'grey',
38 },
39 // ...
40 });
Core APIs, Part 1 260
We can use emojis here for button icons! They require some positioning tweaks in styles.button,
but look decent on both platforms. Later we could swap these out for images or an icon font.
Unfortunately our PDF-creation software doesn’t handle emojis very well, so we couldn’t
include them in the code snippet. You’ll have to grab them from the sample code or choose
emojis of your own!
We define a ToolbarButton component at the top of the file which we can use in the toolbar. This
component is fairly small and styled specifically for use in the toolbar, so we leave it in the same
file as Toolbar. In terms of coding style, it’s common to define small utility components in the same
file that they’re used, and move them into separate files later if we want to reuse them.
Now let’s render a TextInput. As a reminder from the Core Components chapter: when we style a
TextInput, it’s often easier to put styles like border and padding on a wrapper View. Otherwise we
tend to run into slight rendering inconsistencies, e.g. borders not rendering.
1 // ...
2
3 export default class Toolbar extends React.Component {
4 // ...
5
6 state = {
7 text: '',
8 };
9
10 handleChangeText = (text) => {
11 this.setState({ text });
12 };
13
14 handleSubmitEditing = () => {
15 const { onSubmit } = this.props;
16 const { text } = this.state;
17
18 if (!text) return;
19
20 onSubmit(text);
21 this.setState({ text: '' });
22 };
23
24 render() {
25 const { onPressCamera, onPressLocation } = this.props;
26 const { text } = this.state;
27
Core APIs, Part 1 261
28 return (
29 <View style={styles.toolbar}>
30 {/* Use emojis for icons instead! */}
31 <ToolbarButton title={'C'} onPress={onPressCamera} />
32 <ToolbarButton title={'L'} onPress={onPressLocation} />
33 <View style={styles.inputContainer}>
34 <TextInput
35 style={styles.input}
36 underlineColorAndroid={'transparent'}
37 placeholder={'Type something!'}
38 blurOnSubmit={false}
39 value={text}
40 onChangeText={this.handleChangeText}
41 onSubmitEditing={this.handleSubmitEditing}
42 // ...
43 />
44 </View>
45 </View>
46 );
47 }
48 }
49
50 const styles = StyleSheet.create({
51 // ...
52 inputContainer: {
53 flex: 1,
54 flexDirection: 'row',
55 borderWidth: 1,
56 borderColor: 'rgba(0,0,0,0.04)',
57 borderRadius: 16,
58 paddingVertical: 4,
59 paddingHorizontal: 12,
60 backgroundColor: 'rgba(0,0,0,0.02)',
61 },
62 input: {
63 flex: 1,
64 fontSize: 18,
65 },
66 });
Just like in the previous chapter, we store the value of the input field as state.text. When the user
presses the return key on the keyboard, we call onSubmit with this value and then reset state.text.
Our messaging app doesn’t allow sending multiline messages (we’re using the return key to submit,
Core APIs, Part 1 262
so there’s no way to insert a newline).
We use blurOnSubmit={false} so that the keyboard isn’t dismissed when the user presses the return
key. This is common in messaging apps, since it allows sending multiple messages in a row more
easily.
We’ll need more control over the input field’s internal state than we did in the previous chapter.
We’ll need to focus and blur the input field at specific times. We need to do this because when the
user presses the camera icon, we want to dismiss the keyboard. The built-in React Native APIs for
this are imperative: we have to call .focus() and .blur() on an instance of TextInput. We’ll contain
this complexity in this component, so that App can use an isFocused prop to declare the focus state.
In order to do this, we’ll use a ref prop.
Refs
React let’s us access the instance of any component we render using a ref prop. This is a special
prop that we can supply a callback the callback will be called with the instance as a parameter,
after the component mounts (and before it unmounts). We can store a reference to the component
instance.
You can think of a component instance as the “this” when we access this.props or any method
that’s part of our class. In this case, the TextInput component class has a focus and blur method
that can be called from the component instance. We can call these from within the lifecycle of our
custom component to control the focus state of the TextInput.
Storing a ref
Let’s capture a reference to the TextInput element we render.
We’ll store this reference as this.input. We can then use this reference to imperatively focus and
blur the input field with this.input.focus() and this.input.blur() when the isFocused prop
changes.
1 // ...
2
3 export default class Toolbar extends React.Component {
4 // ...
5
6 setInputRef = (ref) => {
7 this.input = ref;
8 };
9
10 componentWillReceiveProps(nextProps) {
11 if (nextProps.isFocused !== this.props.isFocused) {
12 if (nextProps.isFocused) {
Core APIs, Part 1 263
13 this.input.focus();
14 } else {
15 this.input.blur();
16 }
17 }
18 }
19
20 handleFocus = () => {
21 const { onChangeFocus } = this.props;
22
23 onChangeFocus(true);
24 };
25
26 handleBlur = () => {
27 const { onChangeFocus } = this.props;
28
29 onChangeFocus(false);
30 };
31
32 // ...
33
34 render() {
35 const { onPressCamera, onPressLocation } = this.props;
36
37 // Grab this from state!
38 const { text } = this.state;
39
40 return (
41 <View style={styles.toolbar}>
42 {/* Use emojis for icons instead! */}
43 <ToolbarButton title={'C'} onPress={onPressCamera} />
44 <ToolbarButton title={'L'} onPress={onPressLocation} />
45 <View style={styles.inputContainer}>
46 <TextInput
47 style={styles.input}
48 underlineColorAndroid={'transparent'}
49 placeholder={'Type something!'}
50 blurOnSubmit={false}
51 value={text}
52 onChangeText={this.handleChangeText}
53 onSubmitEditing={this.handleSubmitEditing}
54
55 // Additional props!
Core APIs, Part 1 264
56 ref={this.setInputRef}
57 onFocus={this.handleFocus}
58 onBlur={this.handleBlur}
59 />
60 </View>
61 </View>
62 );
63 }
64 }
The onFocus prop of the TextInput will be called when the user taps within the input field, and
the onBlur prop will be called when the user taps outside the input field. We use handleFocus and
handleBlur to notify the parent of changes to the focus state.
Whenever the parent passes a different value for the isFocused prop, we update the focus state of the
TextInput by calling this.input.focus() or this.input.blur() in componentWillReceiveProps.
Go ahead and save Toolbar.js. We can now control the focus state of the toolbar entirely from App
using isFocused and onChangeFocus. We won’t use this much in the next section, but it’ll be very
important when working with the keyboard near the end of the chapter.
Adding Toolbar to App
Let’s now render our Toolbar component from App.js. We can handle the onSubmit event to
populate our message list with real messages. When we type in the input field and submit the text (by
pressing the return key on the keyboard), we can add a new message to the messages in state using
our createTextMessage utility function. We’ll also add a few callback functions as placeholders.
messaging/App.js
import Toolbar from './components/Toolbar';
// ...
export default class App extends React.Component {
state = {
// ...
isInputFocused: false,
}
handlePressToolbarCamera = () => {
// ...
}
handlePressToolbarLocation = () => {
Core APIs, Part 1 265
// ...
}
handleChangeFocus = (isFocused) => {
this.setState({ isInputFocused: isFocused });
};
handleSubmit = (text) => {
const { messages } = this.state;
this.setState({
messages: [createTextMessage(text), ...messages],
});
};
renderToolbar() {
const { isInputFocused } = this.state;
return (
<View style={styles.toolbar}>
<Toolbar
isFocused={isInputFocused}
onSubmit={this.handleSubmit}
onChangeFocus={this.handleChangeFocus}
onPressCamera={this.handlePressToolbarCamera}
onPressLocation={this.handlePressToolbarLocation}
/>
</View>
);
}
// ...
}
// ...
We’ll store isInputFocused in this.state to keep track of the focus state of the TextInput in toolbar.
After saving App.js, our toolbar should look like this:
Core APIs, Part 1 266
There’s an interesting edge case when we open a fullscreen image preview while the input field is
focused the keyboard stays up even though the input field is no longer visible!
Core APIs, Part 1 267
We can address this by updating our
handlePressMessage
function to also set
isInputFocused:
false in the component’s state.
messaging/App.js
// ...
handlePressMessage = ({ id, type }) => {
switch (type) {
// ...
case 'image':
this.setState({ fullscreenImageId: id, isInputFocused: false });
break;
default:
break;
}
};
// ...
Now the keyboard should be dismissed when we tap an image to preview it fullscreen.
Core APIs, Part 1 268
Next, let’s connect the location button so we can send messages containing a map with a pin at our
location.
Geolocation
The React Native geolocation API is slightly different than other APIs: we can access it directly
from the global navigator object, rather than importing it at the top of the file.
The geolocation API in React Native is the same as the one found in modern web browsers. This
means better compatibility between libraries and a lower learning curve if you’re coming from web
development. On the web, the navigator object contains a lot of useful metadata about your web
browser. In React Native, it’s really just a container for geolocation and potentially a handful of
other browser APIs. Accessing a global variable feels a bit unusual in React Native, but is necessary
to provide the exact same API on web and mobile.
We’ll use navigator.geolocation.getCurrentPosition to get our current position. This API takes
a callback parameter which is called with an object containing our coordinates, coords, in latitude
and longitude.
There’s currently a bug in Expo/React Native. This method never calls its callback parameter
on Android. We’ll update this chapter as soon as it’s fixed!
Let’s try it out. We can get our current position and use it to create a location message in the
MessageList. Add the following to handlePressToolbarLocation in App.js:
1 // ...
2
3 handlePressToolbarLocation = () => {
4 const { messages } = this.state;
5
6 navigator.geolocation.getCurrentPosition((position) => {
7 const { coords: { latitude, longitude } } = position;
8
9 this.setState({
10 messages: [
11 createLocationMessage({
12 latitude,
13 longitude,
14 }),
Core APIs, Part 1 269
15 ...messages,
16 ],
17 });
18 });
19 };
20
21 // ...
Pretty simple!
If you try it out, you may be prompted to give Expo permission to access your location. Expo is
already set up to allow the location permission.
If you’re building an app using react-native-cli, you’ll also need to modify your
Info.plist on iOS and AndroidManifest.xml on Android to enable location permissions.
Tapping the location button should now add a location message:
Depending on how we’re using geolocation, there are a few other APIs that might be use-
ful: - watchPosition(success, error?, options?) and clearWatch(watchID) can be used to
receive notifications when location changes. We can also pass the options timeout (number in
Core APIs, Part 1 270
ms), maximumAge (number in ms), and enableHighAccuracy (bool) for more granular control. -
requestAuthorization() can be used to request access to device location. This can be a better expe-
rience than presenting an alert when a map is shown for the first time. - getCurrentPosition(geo_-
success, geo_error?, geo_options?) is the full function signature of the getCurrentPosition API
we use above. Although we didn’t do it in our example, we would generally want to handle errors
and present them to the user in some way. We might also want to pass options for more granular
control (the same options as watchPosition).
Input Method Editor (IME)
We’ve finished creating the message list and toolbar. Let’s move on to one of the more interesting
features of our app: the custom input method editor for sending images.
We’ll build the image grid flexibly so we could easily use it in other apps with just a few
modifications.
Image picker
Let’s populate our image grid with photos from the camera roll. To do this, we’ll need to access the
images saved on the device and display them in an infinitely scrolling grid. We can do this with a
combination of the Core APIs CameraRoll, Dimensions, and PixelRatio.
Core APIs, Part 1 271
Building a grid
Let’s make a new Grid component which we’ll use to display Image components from the camera
roll.
Here we’ll use a FlatList to make our image grid. If you recall from the previous chapter, the
FlatList component is a feature-packed ScrollView that we can use for infinite scrolling out-of-
the-box. The FlatList can be configured to display multiple columns, instead of the normal single
column, by passing the numColumns prop.
Let’s make a new file Grid.js within the components directory. In this, we can create a Grid
component which renders a FlatList. Our Grid will be a wrapper around FlatList that configures
it specifically for our use case: displaying multiple columns of square-shaped components.
Our Grid will pass along all of its props to FlatList, since it’s mostly acting as a more specific case of
the very general FlatList component. Our Grid component will handle three props (two of which
are also FlatList props):
renderItem - A function called with each item. Should return a React element. We want to
intercept this function before passing it to FlatList. We’re going to call it with the same
arguments that FlatList would call it with, but we’re going to add some extra information
about the style of the item to render.
numColumns - The number of columns per row. We’ll use this to calculate item dimensions.
We’ll pass this to FlatList directly.
itemMargin - The vertical and horizontal spacing between each item in the grid. This prop
doesn’t need to be passed into FlatList.
Here’s a skeleton for the Grid component code:
messaging/components/Grid.js
import { Dimensions, FlatList, PixelRatio, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
export default class Grid extends React.Component {
static propTypes = {
renderItem: PropTypes.func.isRequired,
numColumns: PropTypes.number,
itemMargin: PropTypes.number,
};
static defaultProps = {
numColumns: 4,
itemMargin: StyleSheet.hairlineWidth,
Core APIs, Part 1 272
};
renderGridItem = (info) => {
// ... The interesting stuff happens here!
};
render() {
return <FlatList {...this.props} renderItem={this.renderGridItem} />;
}
}
Note that we pass all props into FlatList. We’re building a wrapper around FlatList that adds some
extra rendering info to each item, but we’re going to let FlatList do the hard work of rendering
rows of content in an infinitely scrolling list. We’ll focus on rendering square Image components of
equal size in the renderGridItem function.
We’ll need to get the dimensions of the device using the Dimensions API.
messaging/components/Grid.js
renderGridItem = (info) => {
// ...
const { width } = Dimensions.get('window');
// ...
};
At first glance, it might look like we should call Dimensions.get('window') outside of the render
path, so we only have to retrieve it once. However, width can change sizes depending on device
orientation, multitasking mode, etc, so it’s best to do this within the render path.
Next, let’s calculate the dimensions of each item we’ll render. Here we’ll want to use the PixelRatio.roundToNearestPixel
API. This API helps us ensure we align content to physical pixels when we’re dealing with non-
integer dimensions.
In React Native, we specify dimensions in terms of logical pixels rather than physical
pixels. There may be multiple physical pixels per logical pixels in a device with a high
pixel density, e.g. retina display. When we make calculations that can result in non-integer
dimensions, we should use PixelRatio to help us align to the nearest physical pixel -
otherwise, there may be visual inconsistencies (e.g. some elements or margins appear
larger than others).
Core APIs, Part 1 273
messaging/components/Grid.js
renderGridItem = (info) => {
// ...
const { numColumns, itemMargin } = this.props;
const { width } = Dimensions.get('window');
const size = PixelRatio.roundToNearestPixel(
(width - itemMargin * (numColumns - 1)) / numColumns,
);
// ...
};
We now have a pixel-aligned size for each item in the grid. Let’s also calculate the margins between
elements. We can use the index of the item, which is passed to us automatically by the FlatList.
messaging/components/Grid.js
renderGridItem = (info) => {
const { index } = info;
const { numColumns, itemMargin } = this.props;
const { width } = Dimensions.get('window');
const size = PixelRatio.roundToNearestPixel(
(width - itemMargin * (numColumns - 1)) / numColumns,
);
// We don't want to include a `marginLeft` on the first item of a row
const marginLeft = index % numColumns === 0 ? 0 : itemMargin;
// We don't want to include a `marginTop` on the first row of the grid
const marginTop = index < numColumns ? 0 : itemMargin;
// ...
};
Great! We’ve done all the calculations necessary to start rendering Image elements of the appropriate
size. Let’s call the renderItem prop with this information.
Core APIs, Part 1 274
messaging/components/Grid.js
renderGridItem = (info) => {
const { renderItem, numColumns, itemMargin } = this.props;
// ...
return renderItem({ ...info, size, marginLeft, marginTop });
};
We augment the info passed by FlatList with size, marginLeft, and marginTop, so that we can
render items at the correct size from within the renderItem function.
On generic vs. specific components
What we just did was a little complicated: we created a Grid component which accepts a renderItem
function prop, then we passed a different function, renderGridItem, into the FlatList.
Our goal here is to take the very powerful and generic FlatList and create a more specific version
called Grid. We want to expose nearly all the customizability of FlatList (we propagate all the
props from Grid into FlatList), while tweaking a few parts to make rendering in a grid format
more straightforward. By keeping the API as similar as possible to FlatList, our Grid can be used
as an almost drop-in replacement. Additionally, the learning curve for using our Grid is much lower
than a completely custom component, since if we know the API of FlatList we also know the API
of Grid.
Adding images to the grid
Our grid is ready to go! We wrote the Grid component so that we could add an infinitely scrolling
grid of images for the user to send as messages.
Ultimately we want to fill this grid with photos from the camera roll. But first, let’s try using it with
some placeholder images to test it out.
Create a new file in components called ImageGrid.js. We’ll use our Grid component by adding the
following code:
Core APIs, Part 1 275
messaging/components/ImageGrid.js
import { CameraRoll, Image, StyleSheet, TouchableOpacity } from 'react-native';
import { Permissions } from 'expo';
import PropTypes from 'prop-types';
import React from 'react';
import Grid from './Grid';
const keyExtractor = ({ uri }) => uri;
export default class ImageGrid extends React.Component {
static propTypes = {
onPressImage: PropTypes.func,
};
static defaultProps = {
onPressImage: () => {},
};
state = {
images: [
{ uri: 'https://picsum.photos/600/600?image=10' },
{ uri: 'https://picsum.photos/600/600?image=20' },
{ uri: 'https://picsum.photos/600/600?image=30' },
{ uri: 'https://picsum.photos/600/600?image=40' },
],
};
renderItem = ({ item: { uri }, size, marginTop, marginLeft }) => {
const style = {
width: size,
height: size,
marginLeft,
marginTop,
};
return (
<Image source={{ uri }} style={style} />
);
};
render() {
const { images } = this.state;
Core APIs, Part 1 276
return (
<Grid
data={images}
renderItem={this.renderItem}
keyExtractor={keyExtractor}
// ...
/>
);
}
}
const styles = StyleSheet.create({
image: {
flex: 1,
},
});
You can see that we use Grid in almost the same way as we would use a FlatList. The difference is
that renderItem has a few additional values we can use for layout: size, marginTop, and marginLeft.
Save ImageGrid.js. Let’s render ImageGrid from App.js to see what we have so far. Update App.js
with the following:
messaging/App.js
// ...
import ImageGrid from './components/ImageGrid';
export default class App extends React.Component {
// ...
renderInputMethodEditor = () => (
<View style={styles.inputMethodEditor}>
<ImageGrid />
</View>
);
// ...
}
// ...
Core APIs, Part 1 277
When we save App.js, we should see something similar to this:
On separating components
Notice how by making a separate Grid component, we’ve cleanly separated the grid rendering logic
from the content we render. We could have written the grid rendering logic, and the image loading
from the camera roll in the same ImageGrid component, but then the component would’ve had two
reasonably complex and distinct tasks.
As a general guideline for React Native, it’s useful to separate complex concerns (e.g. rendering
calculations, data fetching) into separate components, so that our components remain focused on a
single task. This makes them easier understand when reading them later, and easier to reuse. Our
Grid can easily be reused in other apps with other kinds of content. Our ImageGrid could render
images into a FlatList instead of a Grid with very few changes.
Loading images from the camera roll
Let’s replace the placeholder images we’ve added to state.images with real images from the camera
roll.
Core APIs, Part 1 278
We can use CameraRoll.getPhotos(options) to request an array of images from the device. We can
specify the number of images we want to get with the first option. We can use a cursor to iterate
through the list of images by passing an after option (more on this soon). This API is asynchronous
and returns a promise containing the image metadata, along with pagination info.
Since this API is asynchronous, it may take some time for the first images to be returned. The more
images we request, the longer it will take. It’s best to request just enough images to fill the entire
screen: we want the API response as soon as we can, but we also want the screen to load all at once,
rather than piecemeal.
Calling CameraRoll.getPhotos(options) returns a promise, which resolves to an object containing:
edges - An array of items, each containing a node object. The node object contains metadata
about the image, such as timestamp and location. The node object also contains an image object
with the filename, width, height, and uri of the image.
page_info - An object containing a boolean has_next_page, a string end_cursor, and a string
before_cursor.
We can pass end_cursor to the after option of CameraRoll.getPhotos(options) in order to iterate
through the list.
If you’re not familiar with using cursors, they’re a common way to iterate through lists
of data stored on servers or in databases. A cursor points to a specific item in a list. By
Core APIs, Part 1 279
passing the cursor along with a request for more items, the server or database will know
which items it’s already returned to you and which it should return next. The details
aren’t too relevant for our uses, but if you’re curious about cursors, you can read more
here
63
.
Before we can access the camera roll on Android, we’ll need to request the user’s permission to do
so. We can use the Expo Permissions API to do this. We can await a call to Permissions.askAsync
containing the permission we want access to, and check the returned object for whether request was
granted.
await Permissions.askAsync(Permissions.CAMERA_ROLL);
if (status !== 'granted') {
// Denied
} else {
// Good to go!
}
More info about permissions is available in the Expo docs
64
.
In componentDidMount, let’s get camera roll permissions, load an initial set of images, and store the
images in state.images.
messaging/components/ImageGrid.js
// ...
export default class ImageGrid extends React.Component {
state = {
images: [],
};
componentDidMount() {
this.getImages();
}
getImages = async () => {
const { status } = await Permissions.askAsync(Permissions.CAMERA_ROLL);
63
https://en.wikipedia.org/wiki/Cursor_(databases)
64
https://docs.expo.io/versions/latest/sdk/permissions.html
Core APIs, Part 1 280
if (status !== 'granted') {
console.log('Camera roll permission denied');
return;
}
const results = await CameraRoll.getPhotos({
first: 20,
});
const { edges } = results;
const loadedImages = edges.map(item => item.node.image);
this.setState({ images: loadedImages });
};
// ...
}
This should give us at most 20 images. If there are fewer than 20 images saved on the device, then
we may see fewer.
We make our getImages method async so that we can use await with CameraRoll.getPhotos.
This works well for 20 images, but now we need to load more when the user scrolls to the bottom
of the Grid. We can use the onEndReached function prop of FlatList (which is also a prop of Grid)
to notify us that we need to load more images. This is trickier than it sounds: the onEndReached
function we pass may be called multiple times before we have finished loading a new set of images.
We need to be careful not to load the same set of images twice. Let’s start by calling getNextImages
when we reach the end of the list:
messaging/components/ImageGrid.js
// ...
export default class ImageGrid extends React.Component {
// ...
getNextImages = () => {
// ...
};
// ...
Core APIs, Part 1 281
render() {
const { images } = this.state;
return (
<Grid
data={images}
renderItem={this.renderItem}
keyExtractor={keyExtractor}
onEndReached={this.getNextImages}
/>
);
}
}
We can use the page_info object in the response of CameraRoll.getPhotos to determine if we need
to load another page. We’ll use:
has_next_page - Are there more images to load?
end_cursor - The cursor we can use to load more images after the current set we’ve just
retrieved.
Let’s keep track of the internal state of the pagination with two member variables, this.loading and
this.cursor. We don’t need to put these in this.state since they don’t directly affect component
rendering. We can also update them synchronously which will make our implementation simpler.
Anytime we use this.setState we have to keep in mind that it occurs asynchronously.
messaging/components/ImageGrid.js
// ...
export default class ImageGrid extends React.Component {
loading = false;
cursor = null;
// ...
getNextImages = () => {
if (!this.cursor) return;
this.getImages(this.cursor);
};
getImages = async (after) => {
Core APIs, Part 1 282
if (this.loading) return;
this.loading = true;
const results = await CameraRoll.getPhotos({
first: 20,
after,
});
const { edges, page_info: { has_next_page, end_cursor } } = results;
const loadedImages = edges.map(item => item.node.image);
this.setState(
{
images: this.state.images.concat(loadedImages),
},
() => {
this.loading = false;
this.cursor = has_next_page ? end_cursor : null;
},
);
};
}
By keeping track of loading, we can be certain that we’ll only ever load one set of images at a time.
We set this.loading = true before making the asynchronous call to getPhotos, and we wait till
the asynchronous call to this.setState() has completed before setting this.loading = false.
The second parameter of this.setState is a completion callback. We can use this to avoid race
conditions between the time we call this.setState and the time this.state is actually updated.
If we didn’t use the completion callback and instead set this.loading = false after calling
this.setState, we would potentially access this.state.images before it had been updated, thus
one set of the images we loaded would fail to be added to the list.
We abort getNextImages if this.cursor doesn’t have a value. This stops us from loading the initial
set of images again once we reach the end of the camera roll. If we preferred, we could instead record
a boolean this.hasNextPage to help us track when we’ve reached the end.
The last thing we’ll do in this file is call the onPressImage prop whenever we tap an image. We’ll
pass the uri of the image so that we can use it within messages.
We’ll wrap the
Image
in a
TouchableOpacity
in order to handle press events. Update
renderItem
with the following:
Core APIs, Part 1 283
messaging/components/ImageGrid.js
// ...
renderItem = ({ item: { uri }, size, marginTop, marginLeft }) => {
const { onPressImage } = this.props;
const style = {
width: size,
height: size,
marginLeft,
marginTop,
};
return (
<TouchableOpacity
key={uri}
activeOpacity={0.75}
onPress={() => onPressImage(uri)}
style={style}
>
<Image source={{ uri }} style={styles.image} />
</TouchableOpacity>
);
};
// ...
Save ImageGrid.js and let’s start using it to add messages to the message list!
Sending images from ImageGrid
We can now load images from the camera roll and display them in a pixel-perfect grid. When
we tap an image, let’s add a new image message to the list of messages in state using our
createImageMessage utility function.
We’ll use handlePressImage to add a new image to the message list:
Core APIs, Part 1 284
messaging/App.js
// ...
export default class App extends React.Component {
// ...
handlePressImage = (uri) => {
const { messages } = this.state;
this.setState({
messages: [createImageMessage(uri), ...messages],
});
};
renderInputMethodEditor = () => (
<View style={styles.inputMethodEditor}>
<ImageGrid onPressImage={this.handlePressImage} />
</View>
);
// ...
}
Great! We can now tap images and they’ll appear in the MessageList we wrote earlier. Here’s what
it should look like:
Core APIs, Part 1 285
What we’ve built
At this point, we have the bulk of the UI components written. In order to access device in-
formation, we’ve used a variety of APIs including Alert, CameraRoll, Dimensions, Geolocation,
NetInfo,PixelRatio, and StatusBar.
Using React Native APIs tends to follow a pattern:
1. Figure out which API we need to call.
2. Figure out which lifecycle/helper method is the most appropriate place to call it.
3. Call the API (either synchronously or asynchronously).
4. Store the results in component state.
5. Re-render the UI based on the new state.
We can use this approach with nearly any API. We’ll continue covering other APIs in the second
part of this chapter, and in the rest of the book.
Core APIs, Part 2
In the first part of this section, we covered a variety of React Native APIs for accessing device
information. In this part, we’ll focus on one fundamental feature of mobile devices: the keyboard.
This is a code checkpoint. If you haven’t been coding along with us but would like to start
now, we’ve included a snapshot of our current progress in the sample code for this book.
If you haven’t created a project yet, you’ll need to do so with:
$ create-react-native-app messaging --scripts-version 1.14.0
Then, copy the contents of the directory messaging/1 from the sample code into your new
messaging project directory.
The keyboard
Keyboard handling in React Native can be very complex. We’re going to learn how to manage the
complexity, but it’s a challenging problem with a lot of nuanced details.
Our UI is currently a bit flawed: on iOS, when we focus the message input field, the keyboard opens
up and covers the toolbar. We have no way of switching between the image picker and the keyboard.
We’ll focus on fixing these issues.
We’re about to embark on a deep dive into keyboard handling. We’ll cover some extremely
useful APIs and patterns however, you shouldn’t feel like you have to complete the entire
chapter now. Feel free to stop here and return again when you’re actively building a React
Native app that involves the keyboard.
Why it’s difficult
Keyboard handling can be challenging for many reasons:
The keyboard is enabled, rendered, and animated natively, so we have much less control over
its behavior than if it were a component (where we control the lifecycle).
We have to handle a variety of asynchronous events when the keyboard is shown, hidden,
or resized, and update our UI accordingly. These events are somewhat different on iOS and
Android, and even slightly different in the simulator compared to a real device.
Core APIs, Part 2 287
The keyboard works differently on iOS and Android at a fundamental level. On iOS, the
keyboard appears on top of the existing UI; the existing UI doesn’t resize to avoid the keyboard.
On Android, the keyboard resizes the UI above it; the existing UI will shrink to fit in the
available space. We generally want interactions to feel similar on both platforms, despite this
fundamental difference.
Keyboards interact specially with certain native elements e.g. ScrollView. On iOS, dragging
downward on a ScrollView can dismiss the keyboard at the same rate of the pan gesture.
Keyboards are user-customizable on both platforms, meaning there’s an almost unlimited
number of shapes and sizes our UI has to handle.
In this app, we’ll attempt to achieve a native-quality messaging experience. Ultimately though, there
will be a few aspects that don’t quite feel native. It’s extremely difficult to get an app with complex
keyboard interactions to feel perfect without dropping down to the native level. If you can’t achieve
the right experience in React Native, consider writing a native module for the screen that interacts
heavily with the keyboard. This is part of the beauty of React Native you can start with a JavaScript
version in your initial implementation of a screen or feature, then seamlessly swap it out for a native
implementation when you’re certain it’s worth the time and effort.
If you’re lucky, you’ll be able to find an existing open source native component that does
exactly that!
KeyboardAvoidingView
In the first chapter, we demonstrated how to use the KeyboardAvoidingView component to move
the UI of the app out from under the keyboard. This component is great for simple use cases, e.g.
focusing the UI on an input field in a form.
When we need more precise control, it’s often better to write something custom. That’s what we’ll
do here, since we need to coordinate the keyboard with our custom image input method.
Our goal here is for our image picker to have the same height as the native keyboard, in essence
acting as a custom keyboard created by our app. We’ll want to smoothly animate the transition
between these two input methods.
For a demo of the desired behavior, you can try playing around with the completed app (it’s the
same app as the previous section):
On Android, you can scan this QR code from within the Expo app:
Core APIs, Part 2 288
On iOS, you can build the app and preview it on the iOS simulator or send the link of the
project URL to your device like we’ve done in previous chapters.
On managing complexity
Since this problem is fairly complicated, we’re going to break it down into 3 parts, each with its own
component:
MeasureLayout - This component will measure the available space for our messaging UI
KeyboardState - This component will keep track of the keyboard’s visibility, height, etc
MessagingContainer - This component will displaying the correct IME (text, images) at the
correct size
We’ll connect them so that MeasureLayout renders KeyboardState, which in turn renders MessagingContainer.
We could build one massive component that handles everything, but this would get very complicated
and be difficult to modify or reuse elsewhere.
Keyboard
We’ll need to measure the available space on the screen and the keyboard height ourselves, and
adjust our UI accordingly. We’ll keep track of whether the keyboard is currently transitioning. And
we’ll animate our UI to transition between the different keyboard states.
To do this, we’ll use the Keyboard API. The Keyboard API is the lower-level API that KeyboardAvoidingView
uses under the hood.
On iOS, the keyboard uses an animation with a special easing curve that’s hard to replicate
in JavaScript, so we’ll hook into the native animation directly using the LayoutAnimation API.
LayoutAnimation is one of the two main ways to animate our UI (the other being Animated). We’ll
cover animation more in a later chapter.
Core APIs, Part 2 289
Measuring the available space
Let’s start by measuring the space we have to work with. We want to measure the space that our
MessageList can use, so we’ll measure from below the status bar (anything above our MessageList)
to the bottom of the screen. We need to do this to get a numeric value for height, so we can transition
between the height when the keyboard isn’t visible to the height when the keyboard is visible. Since
the keyboard doesn’t actually take up any space in our UI, we can’t rely on flex: 1 to take care of
this for us.
Measuring in React Native is always asynchronous. In other words, the first time we render our UI,
we have no general-purpose way of knowing the height. If the content above our MessageList has
a fixed height, we can calculate the initial height by taking Dimensions.get('window').width and
subtracting the height of the content above our MessageList however, this is not very flexible.
Instead, let’s create a container View with a flexible height flex: 1 and measure it on first render.
After that, we’ll always have a numeric value for height.
We can measure this View with the onLayout prop. By passing a callback to onLayout, we can get
the layout of the View. This layout contains values for x, y, width, and height.
messaging/components/MeasureLayout.js
1 import { Constants } from 'expo';
2 import { Platform, StyleSheet, View } from 'react-native';
3 import PropTypes from 'prop-types';
4 import React from 'react';
5
6 export default class MeasureLayout extends React.Component {
7 static propTypes = {
8 children: PropTypes.func.isRequired,
9 };
10
11 state = {
12 layout: null,
13 };
14
15 handleLayout = event => {
16 const { nativeEvent: { layout } } = event;
17
18 this.setState({
19 layout: {
20 ...layout,
21 y:
22 layout.y +
23 (Platform.OS === 'android' ? Constants.statusBarHeight : 0),
24 },
Core APIs, Part 2 290
25 });
26 };
27
28 render() {
29 const { children } = this.props;
30 const { layout } = this.state;
31
32 // Measure the available space with a placeholder view set to flex 1
33 if (!layout) {
34 return <View onLayout={this.handleLayout} style={styles.container} />;
35 }
36
37 return children(layout);
38 }
39 }
40
41 const styles = StyleSheet.create({
42 container: {
43 flex: 1,
44 },
45 });
Here we render a placeholder View with an onLayout prop. When called, we update state with the
new layout.
Most React Native components accept an onLayout function prop. This is conceptually
similar to a React lifecycle method: the function we pass is called every time the component
updates its dimensions. We need to be careful when calling setState within this function,
since setState may cause the component to re-render, in which case onLayout will get called
again… and now we’re stuck in an infinite loop!
We have to compensate for the solid color status bar we use on Android by adjusting the y value,
since the status bar height isn’t included in the layout data. We can do this by merging the existing
properties of layout, ...layout, and an updated y value that includes the status bar height.
We use a new pattern here for propagating the layout into the children of this component: we
require the children prop to be a function. When we use our MeasureLayout component, it will
look something like this:
Core APIs, Part 2 291
<MeasureLayout>
{layout => <View ... />}
</MeasureLayout>
This pattern is similar to having a renderX prop, where X indicates what will be rendered, e.g.
renderMessages. However, using children makes the hierarchy of the component tree more clear.
Using the children prop implies that these children components are the main thing the parent
renders. As an analogy, this pattern is similar to choosing between export default and export X. If
there’s only one variable to export from a file, it’s generally more clear to go with export default. If
there’s a variable with the same name as the file, or a variable that seems like the primary purpose of
the file, you would also likely export it with export default and export other variables with export
X. Similarly, you should consider using children if this prop is the “default” or “primary” thing a
component renders. Ultimately this is an API style preference. Even if you choose not to use it, it’s
useful to be aware of the pattern since you may encounter it when using open source libraries.
We’re now be able to get a precise height which we can use to resize our UI when the keyboard
appears and disappears.
Keyboard events
We have the initial height for our messaging UI, but we need to update the height when the keyboard
appears and disappears. The Keyboard object emits events to let us know when it appears and
disappears. These events contain layout information, and on iOS, information about the animation
that will/did occur.
KeyboardState
Let’s create a new component called KeyboardState to encapsulate the keyboard event handling
logic. For this component, we’re going to use the same pattern as we did for MeasureLayout: we’ll
take a children function prop and call it with information about the keyboard layout.
We can start by figuring out the propTypes for this component. We know we’re going to have
a children function prop. We’re also going to consume the layout from the MeasureLayout
component, and use it in our keyboard height calculations.
Core APIs, Part 2 292
messaging/components/KeyboardState.js
import { Keyboard, Platform } from "react-native";
import PropTypes from 'prop-types';
import React from 'react';
export default class KeyboardState extends React.Component {
static propTypes = {
layout: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
}).isRequired,
children: PropTypes.func.isRequired,
};
// ...
}
Now let’s think about the state. We want to keep track of 6 different values, which we’ll pass into
the children of this component:
contentHeight: The height available for our messaging content.
keyboardHeight: The height of the keyboard. We keep track of this so we set our image picker
to the same size as the keyboard.
keyboardVisible: Is the keyboard fully visible or fully hidden?
keyboardWillShow: Is the keyboard animating into view currently? This is only relevant on
iOS.
keyboardWillHide: Is the keyboard animating out of view currently? This is only relevant on
iOS, and we’ll only use it for fixing visual issues on the iPhone X.
keyboardAnimationDuration: When we animate our UI to avoid the keyboard, we’ll want to
use the same animation duration as the keyboard. Let’s initialize this with the value 250 (in
milliseconds) as an approximation.
Core APIs, Part 2 293
messaging/components/KeyboardState.js
// ...
const INITIAL_ANIMATION_DURATION = 250;
export default class KeyboardState extends React.Component {
// ...
constructor(props) {
super(props);
const { layout: { height } } = props;
this.state = {
contentHeight: height,
keyboardHeight: 0,
keyboardVisible: false,
keyboardWillShow: false,
keyboardWillHide: false,
keyboardAnimationDuration: INITIAL_ANIMATION_DURATION,
};
}
// ...
}
Now that we’ve determined which properties to keep track of, let’s update them based on keyboard
events.
There are 4 Keyboard events we should listen for:
keyboardWillShow (iOS only) - The keyboard is going to appear
keyboardWillHide (iOS only) - The keyboard is going to disappear
keyboardDidShow - The keyboard is now fully visible
keyboardDidHide - The keyboard is now fully hidden
In componentWillMount we can add listeners to each keyboard event and in componentWillUnmount
we can remove them.
Core APIs, Part 2 294
1 // ...
2
3 componentWillMount() {
4 if (Platform.OS === 'ios') {
5 this.subscriptions = [
6 Keyboard.addListener('keyboardWillShow', this.keyboardWillShow),
7 Keyboard.addListener('keyboardWillHide', this.keyboardWillHide),
8 Keyboard.addListener('keyboardDidShow', this.keyboardDidShow),
9 Keyboard.addListener('keyboardDidHide', this.keyboardDidHide),
10 ];
11 } else {
12 this.subscriptions = [
13 Keyboard.addListener('keyboardDidHide', this.keyboardDidHide),
14 Keyboard.addListener('keyboardDidShow', this.keyboardDidShow),
15 ];
16 }
17 }
18
19 componentWillUnmount() {
20 this.subscriptions.forEach(subscription => subscription.remove());
21 }
22
23 // ...
We’ll add the listeners slightly differently for each platform: on Android, we don’t get events for
keyboardWillHide or keyboardWillShow.
Storing subscription handles in an array is a common practice in React Native. We don’t know
exactly how many subscriptions we’ll have until runtime, since it’s different on each platform, so
removing all subscriptions from an array is easier than storing and removing a reference to each
listener callback.
Let’s use these events to update keyboardVisible, keyboardWillShow, and keyboardWillHide in our
state:
messaging/components/KeyboardState.js
// ...
keyboardWillShow = (event) => {
this.setState({ keyboardWillShow: true });
// ...
};